diff --git a/Telegram/BUILD b/Telegram/BUILD index 10de06ab84..59cc632763 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1587,6 +1587,8 @@ plist_fragment( here-location yandexmaps yandexnavi + yandextaxi + yangoride comgooglemaps youtube twitter @@ -1615,6 +1617,7 @@ plist_fragment( dolphin instagram-stories yangomaps + vivaldi LSRequiresIPhoneOS diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 117c2807bb..a626678a02 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -2661,6 +2661,8 @@ Unused sets are archived when you add more."; "Channel.EditAdmin.PermissionInviteViaLink" = "Invite Users via Link"; "Channel.EditAdmin.PermissionPinMessages" = "Pin Messages"; "Channel.EditAdmin.PermissionAddAdmins" = "Add New Admins"; +"Channel.EditAdmin.PermissionProcessJoinRequests" = "Process Join Requests"; +"Channel.EditAdmin.PermissionProcessJoinRequestsInfo" = "Allow this bot to approve new members using its own interfaces, such as captchas."; "Channel.EditAdmin.PermissinAddAdminOn" = "This Admin will be able to add new admins with the same (or more limited) permissions."; "Channel.EditAdmin.PermissinAddAdminOff" = "This Admin will not be able to add new admins."; @@ -2874,6 +2876,13 @@ Unused sets are archived when you add more."; "Call.ReportSend" = "Send"; "Channel.EditAdmin.CannotEdit" = "You cannot edit the rights of this admin."; +"Channel.EditAdmin.GuardBotEnableMembersText" = "New members will now need approval from **%@** before they can send messages in this group"; +"Channel.EditAdmin.GuardBotEnableSubscribersText" = "New subscribers will now need approval from **%@** before they can join this channel"; +"Channel.EditAdmin.GuardBotEnable" = "Enable"; +"Channel.EditAdmin.GuardBotReplaceTitle" = "Replace Bot"; +"Channel.EditAdmin.GuardBotReplaceText" = "**%@** is currently processing join requests. Switch to **%@** instead?"; +"Channel.EditAdmin.GuardBotReplaceKeep" = "Keep %@"; +"Channel.EditAdmin.GuardBotReplaceUse" = "Use %@"; "Call.RateCall" = "Rate This Call"; "Call.ShareStats" = "Share Statistics"; @@ -7020,6 +7029,9 @@ Sorry for the inconvenience."; "InviteLink.Create.RequestApprovalOffInfoChannel" = "New users will be able to join the channel without being approved by the admins."; "InviteLink.Create.RequestApprovalOnInfoGroup" = "New users will be able to join the group only after having been approved by the admins."; "InviteLink.Create.RequestApprovalOnInfoChannel" = "New users will be able to join the channel only after having been approved by the admins."; +"InviteLink.Create.RequestApprovalInfo" = "Request admin approval for people joining through this link."; +"InviteLink.Create.RequestApprovalPublicGroupUnavailable" = "This option is unavailable because anyone can join and send messages through the public group link."; +"InviteLink.ApprovalRequired" = "approval required"; "InviteLink.Create.LinkNameTitle" = "Link Name"; "InviteLink.Create.LinkName" = "Link Name (Optional)"; @@ -7606,6 +7618,15 @@ Sorry for the inconvenience."; "Group.Setup.ApproveNewMembers" = "Approve New Members"; "Group.Setup.ApproveNewMembersInfo" = "Turn this on if you want users to be able to send messages only after they are approved by an admin."; +"Group.Setup.ApproveNewMembersManagedBy" = "Managed by %@"; +"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksTitle" = "Apply to existing invite links?"; +"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksText" = "Also %@ **%@** for **%@ existing invite links** in this %@?"; +"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksEnable" = "enable"; +"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksDisable" = "disable"; +"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksPeerGroup" = "group"; +"Group.Setup.ApproveNewMembersApplyToExistingInviteLinksPeerChannel" = "channel"; +"Channel.Setup.ApproveNewSubscribers" = "Approve New Subscribers"; +"Channel.Setup.ApproveNewSubscribersInfo" = "Turn this on if you want users to be able to join only after they are approved by an admin."; "Gallery.GifSaved" = "GIF Saved"; @@ -16306,3 +16327,18 @@ Error: %8$@"; "VideoChat.StatusPeerJoined" = "%@ joined"; "VideoChat.StatusPeerLeft" = "%@ left"; + +"ChatList.BotConnectionReview.PanelTitle" = "A bot just got access to your messages!"; +"ChatList.BotConnectionReview.PanelText" = "%1$@ connected to your account from %2$@ (%3$@). Was this you?"; + +"RecentSession.ConnectedBot.Subtitle" = "Chat Automation Bot"; +"RecentSession.ConnectedBot.ConnectedFrom" = "CONNECTED FROM"; +"RecentSession.ConnectedBot.Date" = "Date"; +"RecentSession.ConnectedBot.Device" = "Device"; +"RecentSessions.ConnectedBot.ConnectedTime" = "connected %@"; +"RecentSessions.ConnectedBot.TerminateCheckbox" = "Also terminate %@"; + +"Chat.GuestChatMessageTooltip" = "This bot can't read the chat – only the messages where it was mentioned."; + +"MediaPicker.SetNewGroupPhoto" = "Set new group photo"; +"MediaPicker.SetNewChannelPhoto" = "Set new channel photo"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index ab53edec28..bd5b3a8537 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -185,6 +185,22 @@ public enum WallpaperUrlParameter { case gradient([UInt32], Int32?) } +public enum PeerType: Equatable { + case user(isBot: Bool) + case group + case channel + + public static func getType(for peer: EnginePeer) -> PeerType { + if case let .user(user) = peer { + return .user(isBot: user.botInfo != nil) + } else if case let .channel(channel) = peer, case .broadcast = channel.info { + return .channel + } else { + return .group + } + } +} + public struct ResolvedBotChoosePeerTypes: OptionSet { public var rawValue: UInt32 @@ -391,6 +407,16 @@ public enum ResolveUrlResult { case result(ResolvedUrl) } +public struct OpenUserGeneratedUrlConcealedAlertOption { + public let title: String + public let action: () -> Void + + public init(title: String, action: @escaping () -> Void) { + self.title = title + self.action = action + } +} + public enum NavigateToChatKeepStack { case `default` case always @@ -735,7 +761,6 @@ public enum PeerInfoControllerMode { case generic case calls(messages: [EngineMessage]) - case nearbyPeer(distance: Int32) case group(sourceMessageId: EngineMessage.Id) case reaction(EngineMessage.Id) case forumTopic(thread: ChatReplyThreadMessage) @@ -1370,7 +1395,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: EnginePeer.Id, adminId: EnginePeer.Id, 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 @@ -1441,6 +1465,7 @@ public protocol SharedAccountContext: AnyObject { func resolveUrl(context: AccountContext, peerId: EnginePeer.Id?, url: String, skipUrlAuth: Bool) -> Signal func resolveUrlWithProgress(context: AccountContext, peerId: EnginePeer.Id?, url: String, skipUrlAuth: Bool) -> Signal 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: ((EnginePeer.Id, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?, progress: Promise?, completion: (() -> Void)?) + func openUserGeneratedUrl(context: AccountContext, peerId: EnginePeer.Id?, url: String, webpage: TelegramMediaWebpage?, concealed: Bool, forceConcealed: Bool, skipUrlAuth: Bool, skipConcealedAlert: Bool, forceDark: Bool, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise?, alertDisplayUpdated: ((ViewController?) -> Void)?, concealedAlertOption: OpenUserGeneratedUrlConcealedAlertOption?) -> Disposable func openAddContact(context: AccountContext, peer: EnginePeer?, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) func openAddPersonContact(context: AccountContext, peerId: EnginePeer.Id, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) @@ -1468,14 +1493,14 @@ 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 func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController func makeStickerEditorScreen(context: AccountContext, source: Any?, mode: StickerEditorMode, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController - func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?) + func makeAvatarMediaPickerScreen(context: AccountContext, peerType: PeerType, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?) func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController @@ -1521,6 +1546,7 @@ public protocol SharedAccountContext: AnyObject { func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?, verifyAgeCompletion: ((Int) -> Void)?) + func openJoinChatWebView(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, webView: JoinChatWebView) func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController @@ -1530,7 +1556,7 @@ public protocol SharedAccountContext: AnyObject { func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController func makeCocoonInfoScreen(context: AccountContext) -> ViewController - func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController + func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?, TelegramMediaWebpage?) -> Void) -> ViewController @available(iOS 13.0, *) func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController @@ -1637,6 +1663,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/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 39418cacac..09231d3c95 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -264,6 +264,23 @@ public extension ChatMessageItemAssociatedData { return false } } + + func isPollVotingRestricted(poll: TelegramMediaPoll, accountTestingEnvironment: Bool, currentTimestamp: Int32) -> Bool { + if !poll.countries.isEmpty, let accountCountry = self.accountCountry, !poll.countries.contains(accountCountry) { + return true + } + + if poll.restrictToSubscribers { + let period: Int32 = accountTestingEnvironment ? 5 * 60 : 24 * 60 * 60 + if !self.isParticipant { + return true + } else if let invitedOn = self.invitedOn, invitedOn + period > currentTimestamp { + return true + } + } + + return false + } } public enum ChatControllerInteractionLongTapAction { diff --git a/submodules/AdUI/Sources/AdInfoScreen.swift b/submodules/AdUI/Sources/AdInfoScreen.swift index 7cecf8224b..df5a2684fd 100644 --- a/submodules/AdUI/Sources/AdInfoScreen.swift +++ b/submodules/AdUI/Sources/AdInfoScreen.swift @@ -78,7 +78,7 @@ public final class AdInfoScreen: ViewController { self.scrollNode = ASScrollNode() self.scrollNode.view.showsVerticalScrollIndicator = true self.scrollNode.view.showsHorizontalScrollIndicator = false - self.scrollNode.view.scrollsToTop = true + self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.canCancelContentTouches = true if #available(iOS 11.0, *) { diff --git a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm index ba05b9d895..e0d85c8863 100644 --- a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm +++ b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm @@ -341,6 +341,7 @@ textView.opaque = NO; } textView.textContainerInset = self.textContainerInset; + textView.scrollsToTop = false; // Configure textView with UITextInputTraits { @@ -366,6 +367,7 @@ _placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer]; _placeholderTextKitComponents.textView.userInteractionEnabled = NO; _placeholderTextKitComponents.textView.accessibilityElementsHidden = YES; + _placeholderTextKitComponents.textView.scrollsToTop = false; configureTextView(_placeholderTextKitComponents.textView); // Create and configure our text view. diff --git a/submodules/AttachmentUI/BUILD b/submodules/AttachmentUI/BUILD index 2181367a5f..239607ee2e 100644 --- a/submodules/AttachmentUI/BUILD +++ b/submodules/AttachmentUI/BUILD @@ -26,7 +26,6 @@ swift_library( "//submodules/DirectionalPanGesture:DirectionalPanGesture", "//submodules/AttachmentTextInputPanelNode:AttachmentTextInputPanelNode", "//submodules/ChatSendMessageActionUI:ChatSendMessageActionUI", - "//submodules/ChatTextLinkEditUI:ChatTextLinkEditUI", "//submodules/ContextUI:ContextUI", "//submodules/ManagedAnimationNode:ManagedAnimationNode", "//submodules/PhotoResources:PhotoResources", diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index ac85742469..6946cf7fa1 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -33,6 +33,7 @@ public enum AttachmentButtonType: Equatable { case sticker case emoji case audio + case link case standalone public var key: String { @@ -61,6 +62,8 @@ public enum AttachmentButtonType: Equatable { return "emoji" case .audio: return "audio" + case .link: + return "link" case .standalone: return "standalone" } @@ -140,6 +143,12 @@ public enum AttachmentButtonType: Equatable { } else { return false } + case .link: + if case .link = rhs { + return true + } else { + return false + } case .standalone: if case .standalone = rhs { return true diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 7c4e1b3f04..f60228bde9 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -12,7 +12,6 @@ import AccountContext import AttachmentTextInputPanelNode import ChatPresentationInterfaceState import ChatSendMessageActionUI -import ChatTextLinkEditUI import PhotoResources import AnimatedStickerComponent import SemanticStatusNode @@ -244,6 +243,10 @@ private final class AttachButtonComponent: CombinedComponent { case .audio: name = strings.Attachment_Audio imageName = "Chat/Attach Menu/Audio" + case .link: + //TODO:localize + name = "Link" + imageName = "Chat/Attach Menu/Link" case let .app(bot): botPeer = bot.peer name = bot.shortName @@ -1248,7 +1251,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = chatTextLinkEditController(context: strongSelf.context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link in + let controller = strongSelf.context.sharedContext.makeLinkEditController(context: strongSelf.context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link, _ in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link = link { strongSelf.updateChatPresentationInterfaceState(animated: true, { state in @@ -1420,7 +1423,6 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }) }) }, openScheduledMessages: { - }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { @@ -1561,12 +1563,22 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if let data = view.cachedData as? CachedUserData { return data.sendPaidMessageStars } else if let channel = peerViewMainPeer(view) as? TelegramChannel { - if channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { - return nil - } else if let cachedData = view.cachedData as? CachedChannelData, let sendPaidMessageStarsValue = cachedData.sendPaidMessageStars, sendPaidMessageStarsValue == .zero { - return nil + if channel.isMonoForum { + if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { + return nil + } else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars, value == .zero { + return nil + } else { + return channel.sendPaidMessageStars + } } else { - return channel.sendPaidMessageStars + if channel.flags.contains(.isCreator) || channel.adminRights != nil { + return nil + } else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars { + return value == .zero ? nil : value + } else { + return channel.sendPaidMessageStars + } } } else { return nil @@ -1598,6 +1610,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.containerNode.layer.cornerCurve = .continuous } + self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false @@ -1990,6 +2003,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog var width = layout.size.width if buttons.count == 3 { width = smallPanelWidth + } else if buttons.count == 4 { + width = 300 } var panelSideInset: CGFloat @@ -2001,7 +2016,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } var distanceBetweenNodes = floorToScreenPixels((width - panelSideInset * 2.0 - self.buttonSize.width) / CGFloat(max(1, buttons.count - 1))) - if buttons.count == 3 { + if buttons.count == 3 || buttons.count == 4 { distanceBetweenNodes = floorToScreenPixels((width - panelSideInset * 2.0 - 32.0) / CGFloat(max(1, buttons.count - 1))) } let internalWidth = distanceBetweenNodes * CGFloat(max(0, buttons.count - 1)) @@ -2017,7 +2032,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog case .legacy: leftNodeOriginX = (width - internalWidth) / 2.0 } - if buttons.count == 3 { + if buttons.count == 3 || buttons.count == 4 { leftNodeOriginX = floor((layout.size.width - width) / 2.0) + 16.0 } @@ -2171,6 +2186,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog accessibilityTitle = "Emoji" case .audio: accessibilityTitle = self.presentationData.strings.Attachment_Audio + case .link: + //TODO:localize + accessibilityTitle = "Link" case let .app(bot): accessibilityTitle = bot.shortName case .standalone: @@ -2544,6 +2562,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog let buttonsPanelWidth: CGFloat if buttons.count == 3 { buttonsPanelWidth = smallPanelWidth + } else if buttons.count == 4 { + buttonsPanelWidth = 300 } else { buttonsPanelWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0 } diff --git a/submodules/AuthorizationUI/BUILD b/submodules/AuthorizationUI/BUILD index 00851a3e15..4757af3b84 100644 --- a/submodules/AuthorizationUI/BUILD +++ b/submodules/AuthorizationUI/BUILD @@ -47,6 +47,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/BotPaymentsUI/BUILD b/submodules/BotPaymentsUI/BUILD index 45cfed992b..0b35e43a4f 100644 --- a/submodules/BotPaymentsUI/BUILD +++ b/submodules/BotPaymentsUI/BUILD @@ -11,10 +11,21 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/ComponentFlow:ComponentFlow", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/LocalAuth:LocalAuth", "//submodules/AccountContext:AccountContext", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/Components/ResizableSheetComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", "//submodules/ItemListUI:ItemListUI", "//submodules/PasswordSetupUI:PasswordSetupUI", "//submodules/PhotoResources:PhotoResources", diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift index e970e67688..c63254c00e 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift @@ -117,6 +117,7 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode { } else { applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black) } + applePayButton.cornerRadius = BotCheckoutActionButton.height * 0.5 applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside) self.view.addSubview(applePayButton) self.applePayButton = applePayButton diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index 035fa49f99..dab1b83bd0 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -141,9 +141,10 @@ public final class BotCheckoutController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self._hasGlassStyle = true var title = self.presentationData.strings.Checkout_Title if invoice.flags.contains(.isTest) { @@ -151,7 +152,7 @@ public final class BotCheckoutController: ViewController { } self.title = title - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index e81bf2e2a4..ee0a215277 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -1077,13 +1077,13 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz if methods.isEmpty { openNewCard(nil, nil) } else { - strongSelf.present(BotCheckoutPaymentMethodSheetController(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in + strongSelf.controller?.push(BotCheckoutPaymentMethodScreen(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in applyPaymentMethod(method) }, newCard: { openNewCard(nil, nil) }, otherMethod: { url, title in openNewCard(url, title) - }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + })) } } } @@ -1091,14 +1091,14 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz openShippingMethodImpl = { [weak self] in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, !shippingOptions.isEmpty { strongSelf.controller?.view.endEditing(true) - strongSelf.present(BotCheckoutPaymentShippingOptionSheetController(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in + strongSelf.controller?.push(BotCheckoutShippingOptionScreen(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo { strongSelf.currentShippingOptionId = id strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount))) strongSelf.updateActionButton() } - }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + })) } } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift index 84ff228769..0172056d75 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift @@ -206,7 +206,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { } let contentSize = CGSize(width: params.width, height: contentHeight) - let insets = itemListNeighborsPlainInsets(neighbors) + let insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift index bee72bda48..9a178d626c 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoController.swift @@ -60,14 +60,16 @@ final class BotCheckoutInfoController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) + + self._hasGlassStyle = true self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) self.title = self.presentationData.strings.CheckoutInfo_Title - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.rightBarButtonItem = self.doneItem self.doneItem?.isEnabled = false } @@ -84,13 +86,13 @@ final class BotCheckoutInfoController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) }, openCountrySelection: { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true) controller.completeWithCountryCode = { _, id in if let strongSelf = self { strongSelf.controllerNode.updateCountry(id) } } - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + strongSelf.push(controller) } }, updateStatus: { [weak self] status in if let strongSelf = self { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift index 353b1adfbf..cbc2ae8aff 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutInfoControllerNode.swift @@ -455,7 +455,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, ASScrollVi let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) var sideInset: CGFloat = 0.0 - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { sideInset = inset } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift index 74af758711..b1bc797121 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryController.swift @@ -55,14 +55,16 @@ final class BotCheckoutNativeCardEntryController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) + + self._hasGlassStyle = true self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) self.title = self.presentationData.strings.Checkout_NewCard_Title - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.rightBarButtonItem = self.doneItem self.doneItem?.isEnabled = false } @@ -78,13 +80,13 @@ final class BotCheckoutNativeCardEntryController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) }, openCountrySelection: { [weak self] in if let strongSelf = self { - let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false) + let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true) controller.completeWithCountryCode = { _, id in if let strongSelf = self { strongSelf.controllerNode.updateCountry(id) } } - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + strongSelf.push(controller) } }, updateStatus: { [weak self] status in if let strongSelf = self { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift index 7a0c7afc0c..32025f0f50 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutNativeCardEntryControllerNode.swift @@ -468,7 +468,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) var sideInset: CGFloat = 0.0 - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { sideInset = inset } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift index 7e08b985b8..644e8ce4a6 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift @@ -1,374 +1,96 @@ import Foundation import UIKit -import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit import TelegramPresentationData import AccountContext +import ComponentFlow +import AlertComponent +import AlertInputFieldComponent -private final class BotCheckoutPassworInputFieldNode: ASDisplayNode, UITextFieldDelegate { - private var theme: PresentationTheme - private let backgroundNode: ASImageNode - private let textInputNode: TextFieldNode - private let placeholderNode: ASTextNode - - var updateHeight: (() -> Void)? - var complete: (() -> Void)? - var textChanged: ((String) -> Void)? - - private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) - private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) - - var text: String { - get { - return self.textInputNode.textField.text ?? "" - } - set { - self.textInputNode.textField.text = newValue - self.placeholderNode.isHidden = !newValue.isEmpty - } - } - - var placeholder: String = "" { - didSet { - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - } - } - - init(theme: PresentationTheme, placeholder: String) { - self.theme = theme - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - - self.textInputNode = TextFieldNode() - self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor] - self.textInputNode.textField.clipsToBounds = true - self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) - self.textInputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textInputNode.textField.returnKeyType = .done - self.textInputNode.textField.isSecureTextEntry = true - self.textInputNode.textField.tintColor = theme.actionSheet.controlAccentColor - self.textInputNode.textField.textColor = theme.actionSheet.inputTextColor - - self.placeholderNode = ASTextNode() - self.placeholderNode.isUserInteractionEnabled = false - self.placeholderNode.displaysAsynchronously = false - self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - - super.init() - - self.textInputNode.textField.delegate = self - self.textInputNode.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged) - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textInputNode) - self.addSubnode(self.placeholderNode) - } - - func updateTheme(_ theme: PresentationTheme) { - self.theme = theme - - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) - self.textInputNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance - self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) - self.textInputNode.textField.tintColor = self.theme.actionSheet.controlAccentColor - self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor] - self.textInputNode.textField.textColor = self.theme.actionSheet.inputTextColor - } - - func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: width) - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - - let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) - transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) - - transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) - - return panelHeight - } - - func activateInput() { - self.textInputNode.becomeFirstResponder() - } - - func deactivateInput() { - self.textInputNode.resignFirstResponder() - } - - func shake() { - self.layer.addShakeAnimation() +func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let inputState = AlertInputFieldComponent.ExternalState() + let doneInProgressPromise = ValuePromise(false) + let doneIsEnabled: Signal = combineLatest(inputState.valueSignal, doneInProgressPromise.get()) + |> map { value, isInProgress in + return !value.isEmpty && !isInProgress } - @objc func textDidChange() { - self.updateTextNodeText(animated: true) - self.textChanged?(self.textInputNode.textField.text ?? "") - self.placeholderNode.isHidden = !(self.textInputNode.textField.text ?? "").isEmpty - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if text == "\n" { - self.complete?() - return false - } - return true - } - - private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { - let backgroundInsets = self.backgroundInsets - let inputInsets = self.inputInsets - - let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) - - return min(61.0, max(33.0, unboundTextFieldHeight)) - } - - private func updateTextNodeText(animated: Bool) { - let backgroundInsets = self.backgroundInsets - - let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) - - let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight?() - } - } - - @objc func clearPressed() { - self.textInputNode.textField.text = nil - self.deactivateInput() - } -} + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: strings.Checkout_PasswordEntry_Title) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(strings.Checkout_PasswordEntry_Text(cartTitle).string)) + ) + )) -private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { - private let context: AccountContext - private let period: Int32 - private let requiresBiometrics: Bool - private let completion: (TemporaryTwoStepPasswordToken) -> Void - - private let titleNode: ASTextNode - private let textNode: ASTextNode - - private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [TextAlertContentActionNode] - private let actionVerticalSeparators: [ASDisplayNode] - - private let cancelActionNode: TextAlertContentActionNode - private let doneActionNode: TextAlertContentActionNode - - let inputFieldNode: BotCheckoutPassworInputFieldNode - - private var validLayout: CGSize? - private var isVerifying = false - private let disposable = MetaDisposable() - - private let hapticFeedback = HapticFeedback() - - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, passwordTip: String?, cardTitle: String, period: Int32, requiresBiometrics: Bool, cancel: @escaping () -> Void, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) { - self.context = context - self.period = period - self.requiresBiometrics = requiresBiometrics - self.completion = completion - - let alertTheme = AlertControllerTheme(presentationTheme: theme, fontSize: .regular) - - let titleNode = ASTextNode() - titleNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Title, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - titleNode.displaysAsynchronously = false - titleNode.isUserInteractionEnabled = false - titleNode.maximumNumberOfLines = 1 - titleNode.truncationMode = .byTruncatingTail - self.titleNode = titleNode - - self.textNode = ASTextNode() - self.textNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Text(cardTitle).string, font: Font.regular(13.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) - self.textNode.displaysAsynchronously = false - self.textNode.isUserInteractionEnabled = false - - self.inputFieldNode = BotCheckoutPassworInputFieldNode(theme: theme, placeholder: passwordTip ?? "") - - self.actionNodesSeparator = ASDisplayNode() - self.actionNodesSeparator.isLayerBacked = true - self.actionNodesSeparator.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor - - self.cancelActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: { - cancel() - })) - - var doneImpl: (() -> Void)? - self.doneActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .defaultAction, title: strings.Checkout_PasswordEntry_Pay, action: { - doneImpl?() - })) - - self.actionNodes = [self.cancelActionNode, self.doneActionNode] - - var actionVerticalSeparators: [ASDisplayNode] = [] - if self.actionNodes.count > 1 { - for _ in 0 ..< self.actionNodes.count - 1 { - let separatorNode = ASDisplayNode() - separatorNode.isLayerBacked = true - separatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor - actionVerticalSeparators.append(separatorNode) - } - } - self.actionVerticalSeparators = actionVerticalSeparators - - super.init() - - self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) - - self.addSubnode(self.actionNodesSeparator) - - for actionNode in self.actionNodes { - self.addSubnode(actionNode) - } - - for separatorNode in self.actionVerticalSeparators { - self.addSubnode(separatorNode) - } - - self.addSubnode(self.inputFieldNode) - - self.inputFieldNode.textChanged = { [weak self] _ in - if let strongSelf = self { - strongSelf.updateState() - } - } - - self.updateState() - - doneImpl = { [weak self] in - self?.verify() - } + var applyImpl: (() -> Void)? + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertInputFieldComponent( + context: context, + placeholder: passwordTip ?? "", + isSecureTextEntry: true, + autocapitalizationType: .none, + autocorrectionType: .no, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + + var isVerifying = false + let disposable = MetaDisposable() + var dismissImpl: (() -> Void)? + let alertController = AlertScreen( + configuration: AlertScreen.Configuration(allowInputInset: true), + content: content, + actions: [ + .init(title: strings.Common_Cancel), + .init(title: strings.Checkout_PasswordEntry_Pay, type: .default, action: { + applyImpl?() + }, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get()) + ], + updatedPresentationData: (initial: presentationData, signal: context.sharedContext.presentationData) + ) + alertController.dismissed = { _ in + disposable.dispose() } - - override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let previousLayout = self.validLayout - self.validLayout = size - - let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) - - let titleSize = titleNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) - let textSize = self.textNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) - - let actionsHeight: CGFloat = 44.0 - - var minActionsWidth: CGFloat = 0.0 - let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) - let actionTitleInsets: CGFloat = 8.0 - for actionNode in self.actionNodes { - let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionsHeight)) - minActionsWidth += actionTitleSize.width + actionTitleInsets - } - - let contentWidth = max(max(titleSize.width, textSize.width), minActionsWidth) - - let spacing: CGFloat = 6.0 - let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize) - transition.updateFrame(node: titleNode, frame: titleFrame) - - let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) - - let resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom + 46.0) - - let inputFieldWidth = resultSize.width - let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: resultSize.height - 36.0 - actionsHeight - insets.bottom, width: resultSize.width, height: inputFieldHeight)) - - self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) - - var actionOffset: CGFloat = 0.0 - let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) - var separatorIndex = -1 - var nodeIndex = 0 - for actionNode in self.actionNodes { - if separatorIndex >= 0 { - let separatorNode = self.actionVerticalSeparators[separatorIndex] - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) - } - separatorIndex += 1 - - let currentActionWidth: CGFloat - if nodeIndex == self.actionNodes.count - 1 { - currentActionWidth = resultSize.width - actionOffset - } else { - currentActionWidth = actionWidth - } - - let actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight)) - - actionOffset += currentActionWidth - transition.updateFrame(node: actionNode, frame: actionNodeFrame) - - nodeIndex += 1 - } - - if previousLayout == nil { - self.inputFieldNode.activateInput() - } - - return resultSize - } - - @objc func textFieldChanged(_ textField: UITextField) { - self.updateState() - } - - private func updateState() { - var enabled = true - if self.isVerifying || self.inputFieldNode.text.isEmpty { - enabled = false - } - self.doneActionNode.actionEnabled = enabled - } - - private func verify() { - let text = self.inputFieldNode.text - guard !text.isEmpty else { + + applyImpl = { + let password = inputState.value + guard !isVerifying, !password.isEmpty else { return } - - self.isVerifying = true - self.disposable.set((self.context.engine.auth.requestTemporaryTwoStepPasswordToken(password: text, period: self.period, requiresBiometrics: self.requiresBiometrics) |> deliverOnMainQueue).start(next: { [weak self] token in - if let strongSelf = self { - strongSelf.completion(token) - } - }, error: { [weak self] _ in - if let strongSelf = self { - strongSelf.inputFieldNode.shake() - strongSelf.hapticFeedback.error() - strongSelf.isVerifying = false - strongSelf.updateState() - } - })) - self.updateState() - } -} -func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> AlertController { - var dismissImpl: (() -> Void)? - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: BotCheckoutPasswordAlertContentNode(context: context, theme: presentationData.theme, strings: strings, passwordTip: passwordTip, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: { - dismissImpl?() - }, completion: { token in - completion(token) - dismissImpl?() - })) - dismissImpl = { [weak controller] in - controller?.dismissAnimated() + isVerifying = true + doneInProgressPromise.set(true) + + disposable.set((context.engine.auth.requestTemporaryTwoStepPasswordToken(password: password, period: period, requiresBiometrics: requiresBiometrics) + |> deliverOnMainQueue).start(next: { token in + completion(token) + dismissImpl?() + }, error: { _ in + inputState.animateError() + isVerifying = false + doneInProgressPromise.set(false) + })) } - return controller + dismissImpl = { [weak alertController] in + alertController?.dismiss(completion: nil) + } + return alertController } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodScreen.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodScreen.swift new file mode 100644 index 0000000000..1e85b5f263 --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodScreen.swift @@ -0,0 +1,536 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramCore +import AccountContext +import ViewControllerComponent +import ResizableSheetComponent +import TelegramPresentationData +import PresentationDataUtils +import MultilineTextComponent +import ButtonComponent +import ListSectionComponent +import ListActionItemComponent +import GlassBarButtonComponent +import BundleIconComponent + +struct BotCheckoutPaymentWebToken: Equatable { + let title: String + let data: String + var saveOnServer: Bool +} + +enum BotCheckoutPaymentMethod: Equatable { + case savedCredentials(BotPaymentSavedCredentials) + case webToken(BotCheckoutPaymentWebToken) + case applePay + case other(BotPaymentMethod) + + var title: String { + switch self { + case let .savedCredentials(credentials): + switch credentials { + case let .card(_, title): + return title + } + case let .webToken(token): + return token.title + case .applePay: + return "Apple Pay" + case let .other(method): + return method.title + } + } +} + +private func splitSavedCardTitle(_ title: String) -> (String, String?) { + guard let separatorIndex = title.lastIndex(of: "*") else { + return (title, nil) + } + + let name = String(title[.. Void + let addCard: () -> Void + + init( + methods: [BotCheckoutPaymentMethod], + selectedMethod: BotCheckoutPaymentMethod?, + selectMethod: @escaping (BotCheckoutPaymentMethod) -> Void, + addCard: @escaping () -> Void + ) { + self.methods = methods + self.selectedMethod = selectedMethod + self.selectMethod = selectMethod + self.addCard = addCard + } + + static func ==(lhs: BotCheckoutPaymentMethodContentComponent, rhs: BotCheckoutPaymentMethodContentComponent) -> Bool { + if lhs.methods != rhs.methods { + return false + } + if lhs.selectedMethod != rhs.selectedMethod { + return false + } + return true + } + + final class View: UIView { + private let section = ComponentView() + + private var component: BotCheckoutPaymentMethodContentComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BotCheckoutPaymentMethodContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let theme = environment.theme.withModalBlocksBackground() + let itemFontSize: CGFloat = 17.0 + let sideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 76.0 + 9.0 + + var items: [AnyComponentWithIdentity] = [] + for i in 0 ..< component.methods.count { + let method = component.methods[i] + let isSelected = method == component.selectedMethod + + var title = method.title + var icon: ListActionItemComponent.Icon? + var accessory: ListActionItemComponent.Accessory? + + switch method { + case let .savedCredentials(credentials): + switch credentials { + case let .card(_, cardTitle): + let (cardName, cardSuffix) = splitSavedCardTitle(cardTitle) + title = cardName + if let cardSuffix { + accessory = .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"), + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: cardSuffix, + font: Font.regular(itemFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0) + )) + } + } + case .applePay: + title = "Apple Pay" + icon = ListActionItemComponent.Icon( + component: AnyComponentWithIdentity( + id: AnyHashable("apple-pay"), + component: AnyComponent(BundleIconComponent( + name: "Bot Payments/ApplePayLogo", + tintColor: nil + )) + ) + ) + case let .webToken(token): + let (cardName, cardSuffix) = splitSavedCardTitle(token.title) + title = cardName + if let cardSuffix { + accessory = .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"), + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: cardSuffix, + font: Font.regular(itemFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0) + )) + } + case let .other(method): + title = method.title + } + + items.append(AnyComponentWithIdentity(id: AnyHashable("method-\(i)"), component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(itemFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: isSelected, + toggle: { + component.selectMethod(method) + } + )), + icon: icon, + accessory: accessory, + action: { _ in + component.selectMethod(method) + } + )))) + } + + items.append(AnyComponentWithIdentity(id: AnyHashable("add-card"), component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Checkout_PaymentMethod_New, + font: Font.regular(itemFontSize), + textColor: theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + )), + contentInsets: UIEdgeInsets(top: 12.0, left: 45.0, bottom: 12.0, right: 0.0), + leftIcon: nil, + icon: nil, + accessory: nil, + action: { _ in + component.addCard() + } + )))) + + self.section.parentState = state + let sectionSize = self.section.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: nil, + items: items + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize) + if let sectionView = self.section.view { + if sectionView.superview == nil { + self.addSubview(sectionView) + } + transition.setFrame(view: sectionView, frame: sectionFrame) + } + contentHeight += sectionSize.height + contentHeight += 112.0 + + 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 BotCheckoutPaymentMethodScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let currentMethod: BotCheckoutPaymentMethod? + let methods: [BotCheckoutPaymentMethod] + let applyValue: (BotCheckoutPaymentMethod) -> Void + let newCard: () -> Void + let otherMethod: (String, String) -> Void + + init( + context: AccountContext, + currentMethod: BotCheckoutPaymentMethod?, + methods: [BotCheckoutPaymentMethod], + applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, + newCard: @escaping () -> Void, + otherMethod: @escaping (String, String) -> Void + ) { + self.context = context + self.currentMethod = currentMethod + self.methods = methods + self.applyValue = applyValue + self.newCard = newCard + self.otherMethod = otherMethod + } + + static func ==(lhs: BotCheckoutPaymentMethodScreenComponent, rhs: BotCheckoutPaymentMethodScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.currentMethod != rhs.currentMethod { + return false + } + if lhs.methods != rhs.methods { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() + private let animateOut = ActionSlot>() + + private var component: BotCheckoutPaymentMethodScreenComponent? + private weak var state: EmptyComponentState? + private var selectedMethod: BotCheckoutPaymentMethod? + private var isDismissing = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BotCheckoutPaymentMethodScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + if let currentMethod = component.currentMethod, component.methods.contains(currentMethod) { + self.selectedMethod = currentMethod + } else { + self.selectedMethod = nil + } + } + + self.component = component + self.state = state + + let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value + let controller = environmentValue.controller + let theme = environmentValue.theme.withModalBlocksBackground() + + let dismiss: (Bool, (() -> Void)?) -> Void = { [weak self] animated, completion in + guard let self, !self.isDismissing else { + return + } + self.isDismissing = true + + let performDismiss: () -> Void = { + if let controller = controller() as? BotCheckoutPaymentMethodScreen { + controller.completePendingDismiss() + controller.dismiss(animated: false) + } + completion?() + } + + if animated { + self.animateOut.invoke(Action { _ in + performDismiss() + }) + } else { + performDismiss() + } + } + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(ResizableSheetComponent( + content: AnyComponent(BotCheckoutPaymentMethodContentComponent( + methods: component.methods, + selectedMethod: self.selectedMethod, + selectMethod: { [weak self] method in + guard let self else { + return + } + self.selectedMethod = method + self.state?.updated(transition: .spring(duration: 0.35)) + }, + addCard: { [weak self] in + guard let self, let component = self.component else { + return + } + dismiss(true, { + component.newCard() + }) + } + )), + titleItem: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environmentValue.strings.Checkout_PaymentMethod, + font: Font.semibold(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + 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, nil) + } + ) + ), + //TODO:localize + 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("proceed"), + component: AnyComponent(ButtonTextContentComponent( + text: "Proceed", + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: self.selectedMethod != nil, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let selectedMethod = self.selectedMethod else { + return + } + dismiss(true, { + switch selectedMethod { + case let .other(method): + component.otherMethod(method.url, method.title) + default: + component.applyValue(selectedMethod) + } + }) + } + )), + 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: nil, + dismiss: { animated in + dismiss(animated, nil) + } + ) + }, + forceUpdate: true, + 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 { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class BotCheckoutPaymentMethodScreen: ViewControllerComponentContainer { + private var isDismissed = false + private var dismissCompletion: (() -> Void)? + + init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) { + super.init( + context: context, + component: BotCheckoutPaymentMethodScreenComponent( + context: context, + currentMethod: currentMethod, + methods: methods, + applyValue: applyValue, + newCard: newCard, + otherMethod: otherMethod + ), + navigationBarAppearance: .none + ) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func completePendingDismiss() { + let dismissCompletion = self.dismissCompletion + self.dismissCompletion = nil + dismissCompletion?() + } + + func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } + + override func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + 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) + } + } + } +} diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift deleted file mode 100644 index a728d78155..0000000000 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentMethodSheet.swift +++ /dev/null @@ -1,262 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import AccountContext -import AppBundle - -struct BotCheckoutPaymentWebToken: Equatable { - let title: String - let data: String - var saveOnServer: Bool -} - -enum BotCheckoutPaymentMethod: Equatable { - case savedCredentials(BotPaymentSavedCredentials) - case webToken(BotCheckoutPaymentWebToken) - case applePay - case other(BotPaymentMethod) - - var title: String { - switch self { - case let .savedCredentials(credentials): - switch credentials { - case let .card(_, title): - return title - } - case let .webToken(token): - return token.title - case .applePay: - return "Apple Pay" - case let .other(method): - return method.title - } - } -} - -final class BotCheckoutPaymentMethodSheetController: ActionSheetController { - private var presentationDisposable: Disposable? - - init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> 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() - - var items: [ActionSheetItem] = [] - - items.append(ActionSheetTextItem(title: strings.Checkout_PaymentMethod)) - - for method in methods { - let title: String - let icon: UIImage? - switch method { - case let .savedCredentials(credentials): - switch credentials { - case let .card(_, cardTitle): - title = cardTitle - icon = nil - } - case let .webToken(token): - title = token.title - icon = nil - case .applePay: - title = "Apple Pay" - icon = UIImage(bundleImageName: "Bot Payments/ApplePayLogo")?.precomposed() - case let .other(method): - title = method.title - icon = nil - } - let value: Bool? - if let currentMethod = currentMethod { - value = method == currentMethod - } else { - value = nil - } - items.append(BotCheckoutPaymentMethodItem(title: title, icon: icon, value: value, action: { [weak self] _ in - if case let .other(method) = method { - otherMethod(method.url, method.title) - } else { - applyValue(method) - } - self?.dismissAnimated() - })) - } - - items.append(ActionSheetButtonItem(title: strings.Checkout_PaymentMethod_New, action: { [weak self] in - self?.dismissAnimated() - newCard() - })) - - 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() - } -} - -public class BotCheckoutPaymentMethodItem: ActionSheetItem { - public let title: String - public let icon: UIImage? - public let value: Bool? - public let action: (Bool) -> Void - - public init(title: String, icon: UIImage?, value: Bool?, action: @escaping (Bool) -> Void) { - self.title = title - self.icon = icon - self.value = value - self.action = action - } - - public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - let node = BotCheckoutPaymentMethodItemNode(theme: theme) - node.setItem(self) - return node - } - - public func updateNode(_ node: ActionSheetItemNode) { - guard let node = node as? BotCheckoutPaymentMethodItemNode else { - assertionFailure() - return - } - - node.setItem(self) - node.requestLayoutUpdate() - } -} - -public class BotCheckoutPaymentMethodItemNode: ActionSheetItemNode { - private let defaultFont: UIFont - - private let theme: ActionSheetControllerTheme - - private var item: BotCheckoutPaymentMethodItem? - - private let button: HighlightTrackingButton - private let titleNode: ASTextNode - private let iconNode: ASImageNode - private let checkNode: ASImageNode - - public override init(theme: ActionSheetControllerTheme) { - self.theme = theme - self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) - - self.button = HighlightTrackingButton() - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.isUserInteractionEnabled = false - self.titleNode.displaysAsynchronously = false - - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - - self.checkNode = ASImageNode() - self.checkNode.isUserInteractionEnabled = false - self.checkNode.displayWithoutProcessing = true - self.checkNode.displaysAsynchronously = false - self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.controlAccentColor.cgColor) - context.setLineWidth(2.0) - context.move(to: CGPoint(x: 12.0, y: 1.0)) - context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) - context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) - context.strokePath() - }) - - super.init(theme: theme) - - self.view.addSubview(self.button) - self.addSubnode(self.titleNode) - self.addSubnode(self.iconNode) - self.addSubnode(self.checkNode) - - self.button.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.backgroundNode.backgroundColor = theme.itemHighlightedBackgroundColor - } else { - UIView.animate(withDuration: 0.3, animations: { - strongSelf.backgroundNode.backgroundColor = theme.itemBackgroundColor - }) - } - } - } - - self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) - } - - func setItem(_ item: BotCheckoutPaymentMethodItem) { - self.item = item - - self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor) - self.iconNode.image = item.icon - if let value = item.value { - self.checkNode.isHidden = !value - } else { - self.checkNode.isHidden = true - } - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 57.0) - - self.button.frame = CGRect(origin: CGPoint(), size: size) - - var checkInset: CGFloat = 15.0 - if let _ = self.item?.value { - checkInset = 44.0 - } - - let iconSize: CGSize - if let image = self.iconNode.image { - iconSize = image.size - } else { - iconSize = CGSize() - } - let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - iconSize.width - 15.0 - 8.0, height: size.height)) - self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) - self.iconNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - iconSize.width, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) - - if let image = self.checkNode.image { - self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) - } - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc func buttonPressed() { - if let item = self.item { - let updatedValue: Bool - if let value = item.value { - updatedValue = !value - } else { - updatedValue = true - } - item.action(updatedValue) - } - } -} diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift deleted file mode 100644 index 10b86ab94f..0000000000 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPaymentShippingOptionSheetController.swift +++ /dev/null @@ -1,224 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import AccountContext -import TelegramStringFormatting - -final class BotCheckoutPaymentShippingOptionSheetController: ActionSheetController { - private var presentationDisposable: Disposable? - - init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> 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() - - var items: [ActionSheetItem] = [] - - items.append(ActionSheetTextItem(title: strings.Checkout_ShippingMethod)) - - let dismissAction: () -> Void = { [weak self] in - self?.dismissAnimated() - } - - let toggleCheck: (String, Int) -> Void = { [weak self] id, itemIndex in - for i in 0 ..< options.count { - self?.updateItem(groupIndex: 0, itemIndex: i + 1, { item in - if let item = item as? BotCheckoutPaymentShippingOptionItem, let value = item.value { - return BotCheckoutPaymentShippingOptionItem(title: item.title, label: item.label, value: i == itemIndex ? !value : false, action: item.action) - } - return item - }) - } - applyValue(id) - dismissAction() - } - - var itemIndex = 0 - for option in options { - let index = itemIndex - var totalPrice: Int64 = 0 - for price in option.prices { - totalPrice += price.amount - } - let value: Bool? - if let currentId = currentId { - value = option.id == currentId - } else { - value = nil - } - items.append(BotCheckoutPaymentShippingOptionItem(title: option.title, label: formatCurrencyAmount(totalPrice, currency: currency), value: value, action: { value in - toggleCheck(option.id, index) - })) - itemIndex += 1 - } - - 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() - } -} - -public class BotCheckoutPaymentShippingOptionItem: ActionSheetItem { - public let title: String - public let label: String - public let value: Bool? - public let action: (Bool) -> Void - - public init(title: String, label: String, value: Bool?, action: @escaping (Bool) -> Void) { - self.title = title - self.label = label - self.value = value - self.action = action - } - - public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - let node = BotCheckoutPaymentShippingOptionItemNode(theme: theme) - node.setItem(self) - return node - } - - public func updateNode(_ node: ActionSheetItemNode) { - guard let node = node as? BotCheckoutPaymentShippingOptionItemNode else { - assertionFailure() - return - } - - node.setItem(self) - node.requestLayoutUpdate() - } -} - -public class BotCheckoutPaymentShippingOptionItemNode: ActionSheetItemNode { - private let defaultFont: UIFont - - private let theme: ActionSheetControllerTheme - - private var item: BotCheckoutPaymentShippingOptionItem? - - private let button: HighlightTrackingButton - private let titleNode: ASTextNode - private let labelNode: ASTextNode - private let checkNode: ASImageNode - - public override init(theme: ActionSheetControllerTheme) { - self.theme = theme - self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0)) - - self.button = HighlightTrackingButton() - - self.titleNode = ASTextNode() - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.isUserInteractionEnabled = false - self.titleNode.displaysAsynchronously = false - - self.labelNode = ASTextNode() - self.labelNode.maximumNumberOfLines = 1 - self.labelNode.isUserInteractionEnabled = false - self.labelNode.displaysAsynchronously = false - - self.checkNode = ASImageNode() - self.checkNode.isUserInteractionEnabled = false - self.checkNode.displayWithoutProcessing = true - self.checkNode.displaysAsynchronously = false - self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.controlAccentColor.cgColor) - context.setLineWidth(2.0) - context.move(to: CGPoint(x: 12.0, y: 1.0)) - context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) - context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) - context.strokePath() - }) - - super.init(theme: theme) - - self.view.addSubview(self.button) - self.addSubnode(self.titleNode) - self.addSubnode(self.labelNode) - self.addSubnode(self.checkNode) - - self.button.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.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) - } - - func setItem(_ item: BotCheckoutPaymentShippingOptionItem) { - self.item = item - - self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor) - self.labelNode.attributedText = NSAttributedString(string: item.label, font: self.defaultFont, textColor: self.theme.primaryTextColor) - if let value = item.value { - self.checkNode.isHidden = !value - } else { - self.checkNode.isHidden = true - } - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 57.0) - - self.button.frame = CGRect(origin: CGPoint(), size: size) - - var checkInset: CGFloat = 15.0 - if let _ = self.item?.value { - checkInset = 44.0 - } - - let labelSize = self.labelNode.measure(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height)) - let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height)) - self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) - self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) - - if let image = self.checkNode.image { - self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) - } - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } - - @objc func buttonPressed() { - if let item = self.item { - let updatedValue: Bool - if let value = item.value { - updatedValue = !value - } else { - updatedValue = true - } - item.action(updatedValue) - } - } -} diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutShippingOptionScreen.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutShippingOptionScreen.swift new file mode 100644 index 0000000000..aa848c41fc --- /dev/null +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutShippingOptionScreen.swift @@ -0,0 +1,418 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramStringFormatting +import ViewControllerComponent +import ResizableSheetComponent +import TelegramPresentationData +import PresentationDataUtils +import MultilineTextComponent +import ButtonComponent +import ListSectionComponent +import ListActionItemComponent +import GlassBarButtonComponent +import BundleIconComponent + +private final class BotCheckoutShippingOptionContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let currency: String + let options: [BotPaymentShippingOption] + let selectedId: String? + let selectOption: (String) -> Void + + init( + currency: String, + options: [BotPaymentShippingOption], + selectedId: String?, + selectOption: @escaping (String) -> Void + ) { + self.currency = currency + self.options = options + self.selectedId = selectedId + self.selectOption = selectOption + } + + static func ==(lhs: BotCheckoutShippingOptionContentComponent, rhs: BotCheckoutShippingOptionContentComponent) -> Bool { + if lhs.currency != rhs.currency { + return false + } + if lhs.options != rhs.options { + return false + } + if lhs.selectedId != rhs.selectedId { + return false + } + return true + } + + final class View: UIView { + private let section = ComponentView() + + private var component: BotCheckoutShippingOptionContentComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BotCheckoutShippingOptionContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let theme = environment.theme.withModalBlocksBackground() + let itemFontSize: CGFloat = 17.0 + let sideInset: CGFloat = 16.0 + + var contentHeight: CGFloat = 76.0 + 9.0 + + var items: [AnyComponentWithIdentity] = [] + for option in component.options { + let totalPrice = option.prices.reduce(Int64(0)) { current, price in + return current + price.amount + } + let priceText = formatCurrencyAmount(totalPrice, currency: component.currency) + let isSelected = option.id == component.selectedId + + items.append(AnyComponentWithIdentity(id: option.id, component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: option.title, + font: Font.regular(itemFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: isSelected, + toggle: { + component.selectOption(option.id) + } + )), + icon: nil, + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: AnyHashable("price-\(option.id)"), + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: priceText, + font: Font.regular(itemFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0) + )), + action: { _ in + component.selectOption(option.id) + } + )))) + } + + self.section.parentState = state + let sectionSize = self.section.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: nil, + items: items + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize) + if let sectionView = self.section.view { + if sectionView.superview == nil { + self.addSubview(sectionView) + } + transition.setFrame(view: sectionView, frame: sectionFrame) + } + contentHeight += sectionSize.height + contentHeight += 112.0 + + 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 BotCheckoutShippingOptionScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let currency: String + let options: [BotPaymentShippingOption] + let currentId: String? + let applyValue: (String) -> Void + + init( + context: AccountContext, + currency: String, + options: [BotPaymentShippingOption], + currentId: String?, + applyValue: @escaping (String) -> Void + ) { + self.context = context + self.currency = currency + self.options = options + self.currentId = currentId + self.applyValue = applyValue + } + + static func ==(lhs: BotCheckoutShippingOptionScreenComponent, rhs: BotCheckoutShippingOptionScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.currency != rhs.currency { + return false + } + if lhs.options != rhs.options { + return false + } + if lhs.currentId != rhs.currentId { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() + private let animateOut = ActionSlot>() + + private var component: BotCheckoutShippingOptionScreenComponent? + private weak var state: EmptyComponentState? + private var selectedId: String? + private var isDismissing = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BotCheckoutShippingOptionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + if let currentId = component.currentId, component.options.contains(where: { $0.id == currentId }) { + self.selectedId = currentId + } else { + self.selectedId = nil + } + } + + self.component = component + self.state = state + + let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value + let controller = environmentValue.controller + let theme = environmentValue.theme.withModalBlocksBackground() + + let dismiss: (Bool) -> Void = { [weak self] animated in + guard let self, !self.isDismissing else { + return + } + self.isDismissing = true + + let performDismiss: () -> Void = { + if let controller = controller() as? BotCheckoutShippingOptionScreen { + controller.completePendingDismiss() + controller.dismiss(animated: false) + } + } + + if animated { + self.animateOut.invoke(Action { _ in + performDismiss() + }) + } else { + performDismiss() + } + } + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(ResizableSheetComponent( + content: AnyComponent(BotCheckoutShippingOptionContentComponent( + currency: component.currency, + options: component.options, + selectedId: self.selectedId, + selectOption: { [weak self] id in + guard let self else { + return + } + self.selectedId = id + self.state?.updated(transition: .spring(duration: 0.35)) + } + )), + titleItem: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environmentValue.strings.Checkout_ShippingMethod, + font: Font.semibold(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + 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) + } + ) + ), + //TODO:localize + 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("proceed"), + component: AnyComponent(ButtonTextContentComponent( + text: "Proceed", + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: self.selectedId != nil, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let selectedId = self.selectedId else { + return + } + component.applyValue(selectedId) + dismiss(true) + } + )), + 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: nil, + dismiss: { animated in + dismiss(animated) + } + ) + }, + forceUpdate: true, + 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 { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class BotCheckoutShippingOptionScreen: ViewControllerComponentContainer { + private var isDismissed = false + private var dismissCompletion: (() -> Void)? + + init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) { + super.init( + context: context, + component: BotCheckoutShippingOptionScreenComponent( + context: context, + currency: currency, + options: options, + currentId: currentId, + applyValue: applyValue + ), + navigationBarAppearance: .none + ) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func completePendingDismiss() { + let dismissCompletion = self.dismissCompletion + self.dismissCompletion = nil + dismissCompletion?() + } + + func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } + + override func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + 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) + } + } + } +} diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index 4967f82d46..6ca5cb0f41 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -65,7 +65,7 @@ public final class BrowserBookmarksScreen: ViewController { }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in - }, sendMessage: { _ in + }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in @@ -83,8 +83,8 @@ public final class BrowserBookmarksScreen: ViewController { controller.dismiss() } }, openExternalInstantPage: { _ in - }, shareCurrentLocation: { - }, shareAccountContact: { + }, shareCurrentLocation: { _ in + }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { message, _ in if let openMessageImpl = openMessageImpl { @@ -134,7 +134,7 @@ public final class BrowserBookmarksScreen: ViewController { }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in @@ -402,7 +402,7 @@ public final class BrowserBookmarksScreen: ViewController { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.title = self.presentationData.strings.WebBrowser_Bookmarks_Title self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index ed1ef99ea2..af5e88e55b 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -166,6 +166,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.delegate = self + self.scrollNode.view.scrollsToTop = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never @@ -1532,12 +1533,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in if let self { self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } }) - self.present(actionSheet, nil) + self.push(actionSheet) } private func openMedia(_ media: InstantPageMedia) { diff --git a/submodules/BrowserUI/Sources/BrowserReadability.swift b/submodules/BrowserUI/Sources/BrowserReadability.swift index 5a388762d6..8875493226 100644 --- a/submodules/BrowserUI/Sources/BrowserReadability.swift +++ b/submodules/BrowserUI/Sources/BrowserReadability.swift @@ -34,7 +34,40 @@ public class Readability: NSObject, WKNavigationDelegate { if let (html, subresources) = extractHtmlString(from: archiveData) { self.subresources = subresources - self.webView.loadHTMLString(html, baseURL: url.baseURL) + self.sanitizeHtmlString(html) { [weak self] html in + guard let self else { + return + } + self.webView.loadHTMLString(html, baseURL: url.baseURL) + } + } + } + + private func sanitizeHtmlString(_ html: String, completion: @escaping (String) -> Void) { + guard let readerModeJS = loadFile(name: "ReaderMode", type: "js") else { + completion(htmlByRemovingScriptTags(html)) + return + } + + let domPurifyJS = extractDOMPurifyScript(from: readerModeJS) ?? readerModeJS + self.webView.evaluateJavaScript(domPurifyJS) { [weak self] _, error in + guard let self else { + return + } + + guard error == nil, let htmlLiteral = javascriptStringLiteral(html) else { + completion(htmlByRemovingScriptTags(html)) + return + } + + let sanitizeJS = """ + (function(html) { + return DOMPurify.sanitize(html, {WHOLE_DOCUMENT: true, ADD_TAGS: ["iframe"]}); + })(\(htmlLiteral)); + """ + self.webView.evaluateJavaScript(sanitizeJS) { result, _ in + completion((result as? String) ?? htmlByRemovingScriptTags(html)) + } } } @@ -49,6 +82,7 @@ public class Readability: NSObject, WKNavigationDelegate { return } guard let page = parseJson(result, url: self.url.absoluteString) else { + completion(nil, error) return } completion(page, nil) @@ -95,6 +129,32 @@ func loadFile(name: String, type: String) -> String? { return userScript } +private func extractDOMPurifyScript(from readerModeJS: String) -> String? { + guard let range = readerModeJS.range(of: "\n\n(function () {") else { + return nil + } + return String(readerModeJS[.. String? { + guard let data = try? JSONSerialization.data(withJSONObject: [input], options: []), + var arrayString = String(data: data, encoding: .utf8), + arrayString.count >= 2 else { + return nil + } + arrayString.removeFirst() + arrayString.removeLast() + return arrayString +} + +private func htmlByRemovingScriptTags(_ input: String) -> String { + guard let regex = try? NSRegularExpression(pattern: "]*>[\\s\\S]*?", options: [.caseInsensitive]) else { + return input + } + let range = NSRange(input.startIndex ..< input.endIndex, in: input) + return regex.stringByReplacingMatches(in: input, options: [], range: range, withTemplate: "") +} + private func extractHtmlString(from webArchiveData: Data) -> (String, [Any]?)? { if let webArchiveDict = try? PropertyListSerialization.propertyList(from: webArchiveData, format: nil) as? [String: Any], let mainResource = webArchiveDict["WebMainResource"] as? [String: Any], @@ -724,30 +784,38 @@ private func parseVideo(_ input: [String: Any], _ media: inout [EngineMedia.Id: ) } +private func firstElement(withTag tag: String, in input: [Any], skippingSubtreesWithTag skippedTag: String? = nil) -> [String: Any]? { + for item in input { + guard let item = item as? [String: Any] else { + continue + } + let itemTag = item["tag"] as? String + if itemTag == tag { + return item + } + if itemTag == skippedTag { + continue + } + if let content = item["content"] as? [Any], let result = firstElement(withTag: tag, in: content, skippingSubtreesWithTag: skippedTag) { + return result + } + } + return nil +} + private func parseFigure(_ input: [String: Any], _ media: inout [EngineMedia.Id: EngineRawMedia]) -> InstantPageBlock? { guard let content = input["content"] as? [Any] else { return nil } var block: InstantPageBlock? var caption: RichText? - for item in content { - if let item = item as? [String: Any], let tag = item["tag"] as? String { - if tag == "p", let content = item["content"] as? [Any] { - for item in content { - if let item = item as? [String: Any], let tag = item["tag"] as? String { - if tag == "iframe" { - block = parseVideo(item, &media) - } - } - } - } else if tag == "iframe" { - block = parseVideo(item, &media) - } else if tag == "img" { - block = parseImage(item, &media) - } else if tag == "figcaption" { - caption = trim(parseRichText(item, &media)) - } - } + if let iframe = firstElement(withTag: "iframe", in: content, skippingSubtreesWithTag: "figcaption") { + block = parseVideo(iframe, &media) + } else if let image = firstElement(withTag: "img", in: content, skippingSubtreesWithTag: "figcaption") { + block = parseImage(image, &media) + } + if let figcaption = firstElement(withTag: "figcaption", in: content) { + caption = trim(parseRichText(figcaption, &media)) } guard var block else { return nil diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 46503f1a7e..ce7f1a6747 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -1228,7 +1228,7 @@ public class BrowserScreen: ViewController, MinimizableController { ) var defaultWebBrowser: String? = settings.defaultWebBrowser - if defaultWebBrowser == nil || defaultWebBrowser == "inAppSafari" { + if defaultWebBrowser == nil || defaultWebBrowser == "inApp" || defaultWebBrowser == "inAppSafari" { defaultWebBrowser = "safari" } @@ -1265,6 +1265,7 @@ public class BrowserScreen: ViewController, MinimizableController { } else { items.append(.custom(fontItem, false)) + items.append(.separator) if case .webPage = contentState.contentType { let isAvailable = contentState.hasInstantView @@ -1280,6 +1281,17 @@ public class BrowserScreen: ViewController, MinimizableController { } } + if toolbarMode != .markdown && [.webPage, .instantPage].contains(contentState.contentType) { + if !layout.metrics.isTablet && canOpenIn { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in + if let self { + self.context.sharedContext.applicationBindings.openUrl(openInUrl) + } + action(.default) + }))) + } + } + if !items.isEmpty { items.append(.separator) } @@ -1290,12 +1302,6 @@ public class BrowserScreen: ViewController, MinimizableController { action(.default) }))) } - if [.webPage, .document].contains(contentState.contentType) { - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in - performAction.invoke(.updateSearchActive(true)) - action(.default) - }))) - } if canShare && !layout.metrics.isTablet { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in @@ -1304,7 +1310,16 @@ public class BrowserScreen: ViewController, MinimizableController { }))) } + if [.webPage, .document].contains(contentState.contentType) { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in + performAction.invoke(.updateSearchActive(true)) + action(.default) + }))) + } + if toolbarMode != .markdown && [.webPage, .instantPage].contains(contentState.contentType) { + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in performAction.invoke(.addBookmark) action(.default) @@ -1316,15 +1331,6 @@ public class BrowserScreen: ViewController, MinimizableController { action(.default) }))) } - - if !layout.metrics.isTablet && canOpenIn { - items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in - if let self { - self.context.sharedContext.applicationBindings.openUrl(openInUrl) - } - action(.default) - }))) - } } return ContextController.Items(content: .list(items)) } diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index eb51c84e4b..28bf96d2a9 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -1868,7 +1868,7 @@ public final class CalendarMessageScreen: ViewController { self._hasGlassStyle = true self.navigationPresentation = .modal - self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false) + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(dismissPressed)), animated: false) self.navigationItem.setTitle(self.presentationData.strings.MessageCalendar_Title, animated: false) if self.enableMessageRangeDeletion { @@ -1894,7 +1894,7 @@ public final class CalendarMessageScreen: ViewController { self.node.toggleSelectionMode() if self.node.selectionState != nil { - self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true) + self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true) } else { self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.toggleSelectPressed)), animated: true) } @@ -1904,7 +1904,7 @@ public final class CalendarMessageScreen: ViewController { self.node.selectDay(timestamp: timestamp) if self.node.selectionState != nil { - self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true) + self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true) } } diff --git a/submodules/CallListUI/Sources/CallListCallItem.swift b/submodules/CallListUI/Sources/CallListCallItem.swift index 507eeb5dd1..9f9b853ea7 100644 --- a/submodules/CallListUI/Sources/CallListCallItem.swift +++ b/submodules/CallListUI/Sources/CallListCallItem.swift @@ -785,7 +785,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode { strongSelf.view.accessibilityCustomActions = [UIAccessibilityCustomAction(name: item.presentationData.strings.Common_Delete, target: strongSelf, selector: #selector(strongSelf.performLocalAccessibilityCustomAction(_:)))] - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)])) strongSelf.setRevealOptionsOpened(item.revealed, animated: animated) } }) diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 69e8defa35..97ec0f7cd9 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -213,7 +213,7 @@ public final class CallListController: TelegramBaseController { if let isEmpty = self.isEmpty, isEmpty { } else { if self.editingMode { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) } else { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) } @@ -222,7 +222,7 @@ public final class CallListController: TelegramBaseController { //self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) case .navigation: if self.editingMode { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) } else { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) } @@ -679,7 +679,7 @@ public final class CallListController: TelegramBaseController { switch self.mode { case .tab: - self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true) + self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true) self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true) self.navigationItem.rightBarButtonItem?.setCustomAction({ @@ -691,7 +691,7 @@ public final class CallListController: TelegramBaseController { pressedImpl?() }) - self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true) + self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true) } self.controllerNode.updateState { state in diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index b4b90095fa..ca0a001822 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -940,7 +940,7 @@ final class CallListControllerNode: ASDisplayNode { insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) let inset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { inset = 0.0 diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index d3b4bd3af6..ed6f05ff25 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -125,9 +125,11 @@ swift_library( "//submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode", "//submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent", "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertHeaderComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", "//submodules/TelegramUI/Components/AvatarComponent", "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/GlassControls", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 24b23d24e6..0d0908f79e 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -503,14 +503,26 @@ func chatContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, promoI joinChannelDisposable.set(nil) } + var didJoin = false joinChannelDisposable.set((createSignal - |> deliverOnMainQueue).start(next: { _ in + |> deliverOnMainQueue).start(next: { result in + switch result { + case .joined: + didJoin = true + case let .webView(webView): + if let chatListController = chatListController { + context.sharedContext.openJoinChatWebView(context: context, parentController: chatListController, updatedPresentationData: nil, webView: webView) + } + } }, error: { _ in if let chatListController = chatListController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) } }, completed: { + if !didJoin { + return + } let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).startStandalone(next: { peer in guard let peer = peer else { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 6841b665d0..79b806e43d 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -59,6 +59,9 @@ import ChatListFilterTabContainerNode import HeaderPanelContainerComponent import HorizontalTabsComponent import GlobalControlPanelsContext +import AlertComponent +import AlertHeaderComponent +import AvatarComponent private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController @@ -5199,15 +5202,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController |> delay(0.8, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - let signal: Signal = self.context.peerChannelMemberCategoriesContextsManager.join(engine: self.context.engine, peerId: peerId, hash: nil) + let signal: Signal = self.context.peerChannelMemberCategoriesContextsManager.join(engine: self.context.engine, peerId: peerId, hash: nil) |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } + var didJoin = false self.joinForumDisposable.set((signal - |> deliverOnMainQueue).startStrict(error: { [weak self] error in + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self else { + return + } + switch result { + case .joined: + didJoin = true + case let .webView(webView): + self.context.sharedContext.openJoinChatWebView(context: self.context, parentController: self, updatedPresentationData: nil, webView: webView) + } + }, error: { [weak self] error in guard let strongSelf = self else { return } @@ -5241,36 +5255,39 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { [weak self] in - guard let self else { - return - } - Queue.mainQueue().after(0.5) { - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self, let peer = peer?._asPeer() else { - return - } - var canEditRank = false - if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.editRank) { - canEditRank = true - } else if let group = peer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) { - canEditRank = true - } - if canEditRank { - let context = self.context - let controller = UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.presentationData.strings.Chat_JoinedGroup_Text, cancel: self.presentationData.strings.Chat_JoinedGroup_AddTag, destructive: false), elevatedLayout: true, action: { [weak self] action in - if let self, case .undo = action { - let tagController = context.sharedContext.makeChatCustomRankSetupScreen(context: context, peerId: peerId, participantId: context.account.peerId, rank: nil, role: .member) - self.push(tagController) - } - return true - }) - self.present(controller, in: .current) - } - }) - } }) + }, completed: { [weak self] in + guard let self else { + return + } + if !didJoin { + return + } + Queue.mainQueue().after(0.5) { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in + guard let self, let peer = peer?._asPeer() else { + return + } + var canEditRank = false + if let channel = peer as? TelegramChannel, case .group = channel.info, channel.hasPermission(.editRank) { + canEditRank = true + } else if let group = peer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) { + canEditRank = true + } + if canEditRank { + let context = self.context + let controller = UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.presentationData.strings.Chat_JoinedGroup_Text, cancel: self.presentationData.strings.Chat_JoinedGroup_AddTag, destructive: false), elevatedLayout: true, action: { [weak self] action in + if let self, case .undo = action { + let tagController = context.sharedContext.makeChatCustomRankSetupScreen(context: context, peerId: peerId, participantId: context.account.peerId, rank: nil, role: .member) + self.push(tagController) + } + return true + }) + self.present(controller, in: .current) + } + }) + } })) case .savedMessagesChats: break @@ -5375,21 +5392,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 { @@ -5842,30 +5856,32 @@ 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] = [] - - items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) - + var actions: [AlertScreen.Action] = [] if joined || mainPeer.isDeleted { - items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() + actions.append(.init(title: self.presentationData.strings.Common_Delete, type: .defaultDestructive, action: { [weak self] in self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { removed() }) completion(true) })) } else { - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() + actions.append(.init(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, type: .destructive, action: { [weak self] in self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { removed() }) completion(true) })) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).string, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() + + actions.append(.init(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).string, type: .destructive, action: { [weak self] in guard let strongSelf = self else { return } @@ -5882,16 +5898,107 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ], parseMarkdown: true), in: .window(.root)) })) } - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - completion(false) - }) - ]) - ]) - self.present(actionSheet, in: .window(.root)) + actions.append(.init(title: self.presentationData.strings.Common_Cancel)) + + //TODO:localize + let title: String = "Delete Chat" + var text: String + if mainPeer.id == self.context.account.peerId { + text = self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmation + } else if case let .legacyGroup(chatPeer) = mainPeer { + text = self.presentationData.strings.ChatList_LeaveGroupConfirmation("**\(chatPeer.title)**").string + } else if case let .channel(chatPeer) = mainPeer { + text = self.presentationData.strings.ChatList_LeaveGroupConfirmation("**\(chatPeer.title)**").string + } else if case .secretChat = chatPeer { + text = self.presentationData.strings.ChatList_DeleteSecretChatConfirmation("**\(chatPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))**").string + } else { + text = self.presentationData.strings.ChatList_DeleteChatConfirmation("**\(chatPeer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))**").string + } + + let alertScreen = AlertScreen( + context: self.context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: [ + AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertHeaderComponent( + component: AnyComponentWithIdentity(id: "user", component: AnyComponent( + AvatarComponent( + context: self.context, + theme: self.presentationData.theme, + peer: mainPeer + ) + )) + ) + ) + ), + AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: title) + ) + ), + AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + ) + ], + actions: actions + ) + + +// let actionSheet = ActionSheetController(presentationData: self.presentationData) +// var items: [ActionSheetItem] = [] +// +// items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) +// +// if joined || mainPeer.isDeleted { +// items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in +// actionSheet?.dismissAnimated() +// self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { +// removed() +// }) +// completion(true) +// })) +// } else { +// items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in +// actionSheet?.dismissAnimated() +// self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { +// removed() +// }) +// completion(true) +// })) +// items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).string, color: .destructive, action: { [weak self, weak actionSheet] in +// actionSheet?.dismissAnimated() +// guard let strongSelf = self else { +// return +// } +// strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [ +// TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { +// completion(false) +// }), +// TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: { +// self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { +// removed() +// }) +// completion(true) +// }) +// ], parseMarkdown: true), in: .window(.root)) +// })) +// } +// actionSheet.setItemGroups([ +// ActionSheetItemGroup(items: items), +// ActionSheetItemGroup(items: [ +// ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// completion(false) +// }) +// ]) +// ]) + self.present(alertScreen, in: .window(.root)) } else if peer.peerId == self.context.account.peerId { self.present(textAlertController(context: self.context, title: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: self.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { @@ -5945,6 +6052,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 @@ -6126,7 +6243,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case .chatList(.root) = self.chatListDisplayNode.mainContainerNode.location { super.setToolbar(toolbar, transition: transition) } else { - self.chatListDisplayNode.toolbar = toolbar + self.chatListDisplayNode.toolbarData = toolbar self.requestLayout(transition: transition) } } @@ -7046,6 +7163,7 @@ private final class ChatListLocationContext { if case .chatList(.root) = self.location { self.rightButton = nil self.storyButton = nil + self.proxyButton = nil } let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle @@ -7061,6 +7179,7 @@ private final class ChatListLocationContext { if case .chatList(.root) = self.location { self.rightButton = nil self.storyButton = nil + self.proxyButton = nil } self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( content: .text(title: presentationData.strings.Common_Done, isBold: true), diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 826aa80f86..61cbefe701 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -28,6 +28,7 @@ import MediaPlaybackHeaderPanelComponent import LiveLocationHeaderPanelComponent import ChatListHeaderNoticeComponent import ChatListFilterTabContainerNode +import GlassControls public enum ChatListContainerNodeFilter: Equatable { case all @@ -1134,8 +1135,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { let navigationBarView = ComponentView() weak var controller: ChatListControllerImpl? - var toolbar: Toolbar? - private var toolbarNode: ToolbarNode? + private var toolbar: ComponentView? + var toolbarData: Toolbar? var toolbarActionSelected: ((ToolbarActionOption) -> Void)? private var isSearchDisplayControllerActive: ChatListNavigationBar.ActiveSearch? @@ -1395,10 +1396,6 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self.mainContainerNode.updatePresentationData(presentationData) self.inlineStackContainerNode?.updatePresentationData(presentationData) self.searchDisplayController?.updatePresentationData(presentationData) - - if let toolbarNode = self.toolbarNode { - toolbarNode.updateTheme(ToolbarTheme(rootControllerTheme: self.presentationData.theme)) - } } private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: ComponentTransition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) { @@ -1435,6 +1432,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self.effectiveContainerNode.currentItemNode.interaction?.openPremiumGift(peers, birthdays) case .reviewLogin: break + case .reviewBotConnection: + break case let .starsSubscriptionLowBalance(amount, _): self.effectiveContainerNode.currentItemNode.interaction?.openStarsTopup(amount.value) case .setupPhoto: @@ -1458,6 +1457,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { switch notice { case let .reviewLogin(newSessionReview, _): self.effectiveContainerNode.currentItemNode.interaction?.performActiveSessionAction(newSessionReview, isPositive) + case let .reviewBotConnection(newBotConnectionReview, _, _): + self.effectiveContainerNode.currentItemNode.interaction?.performBotConnectionReviewAction(newBotConnectionReview, isPositive) default: break } @@ -1838,53 +1839,101 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { insets.left += layout.safeInsets.left insets.right += layout.safeInsets.right - if let toolbar = self.toolbar { - var tabBarHeight: CGFloat - var options: ContainerViewLayoutInsetOptions = [] - if layout.metrics.widthClass == .regular { - options.insert(.input) + if let toolbarData = self.toolbarData { + var panelsBottomInset: CGFloat = layout.insets(options: []).bottom + if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 { + panelsBottomInset = inputHeight + 8.0 } - - var heightInset: CGFloat = 0.0 - if case .forum = self.location { - heightInset = 4.0 - } - - let bottomInset: CGFloat = layout.insets(options: options).bottom - if !layout.safeInsets.left.isZero { - tabBarHeight = 34.0 + bottomInset - insets.bottom += 34.0 + if panelsBottomInset == 0.0 { + panelsBottomInset = 8.0 } else { - tabBarHeight = 49.0 - heightInset + bottomInset - insets.bottom += 49.0 - heightInset + panelsBottomInset = max(panelsBottomInset, 8.0) } - let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) + let sideInset: CGFloat = 20.0 + let toolbarHeight = 44.0 + let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight)) - if let toolbarNode = self.toolbarNode { - transition.updateFrame(node: toolbarNode, frame: toolbarFrame) - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition) + let toolbar: ComponentView + var toolbarTransition = ComponentTransition(transition) + if let current = self.toolbar { + toolbar = current } else { - let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: self.presentationData.theme), displaySeparator: true, left: { [weak self] in - self?.toolbarActionSelected?(.left) - }, right: { [weak self] in - self?.toolbarActionSelected?(.right) - }, middle: { [weak self] in - self?.toolbarActionSelected?(.middle) - }) - toolbarNode.frame = toolbarFrame - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate) - self.addSubnode(toolbarNode) - self.toolbarNode = toolbarNode - if transition.isAnimated { - toolbarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + toolbar = ComponentView() + self.toolbar = toolbar + toolbarTransition = .immediate + } + + let _ = toolbar.update( + transition: toolbarTransition, + component: AnyComponent(GlassControlPanelComponent( + theme: self.presentationData.theme, + leftItem: toolbarData.leftAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "left_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self else { + return + } + self.toolbarActionSelected?(.left) + } : nil + )], + background: .panel + ) + }, + centralItem: toolbarData.middleAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "right_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self else { + return + } + self.toolbarActionSelected?(.middle) + } : nil + )], + background: .panel + ) + }, + rightItem: toolbarData.rightAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "right_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self else { + return + } + self.toolbarActionSelected?(.right) + } : nil + )], + background: .panel + ) + }, + centerAlignmentIfPossible: true + )), + environment: {}, + containerSize: toolbarFrame.size + ) + + if let toolbarView = toolbar.view { + if toolbarView.superview == nil { + self.view.addSubview(toolbarView) + toolbarView.alpha = 0.0 } + toolbarTransition.setFrame(view: toolbarView, frame: toolbarFrame) + ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 1.0) + } + } else if let toolbar = self.toolbar { + self.toolbar = nil + if let toolbarView = toolbar.view { + ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 0.0, completion: { [weak toolbarView] _ in + toolbarView?.removeFromSuperview() + }) } - } else if let toolbarNode = self.toolbarNode { - self.toolbarNode = nil - transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { [weak toolbarNode] _ in - toolbarNode?.removeFromSupernode() - }) } var childrenLayout = layout diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift index 6b75d9a15d..98fd3fb9f6 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetCategoryItem.swift @@ -21,7 +21,7 @@ enum ChatListFilterCategoryIcon { case archived } -final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem { +final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem { let presentationData: ItemListPresentationData let systemStyle: ItemListSystemStyle let title: String @@ -31,6 +31,10 @@ final class ChatListFilterPresetCategoryItem: ListViewItem, ItemListItem { let sectionId: ItemListSectionId let updatedRevealedOptions: (Bool) -> Void let remove: () -> Void + + var hasActiveRevealOptions: Bool { + return self.isRevealed + } init( presentationData: ItemListPresentationData, @@ -110,6 +114,7 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL private var item: ChatListFilterPresetCategoryItem? private var layoutParams: ListViewItemLayoutParams? + private var isHighlighted = false private var editableControlNode: ItemListEditableControlNode? @@ -177,7 +182,7 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL var titleAttributedString: NSAttributedString? let peerRevealOptions: [ItemListRevealOption] - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)] let rightInset: CGFloat = params.rightInset @@ -326,26 +331,29 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false + let topStripeIsHidden: Bool switch neighbors.top { case .sameSection(false): - strongSelf.topStripeNode.isHidden = true + topStripeIsHidden = true default: hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners + topStripeIsHidden = hasCorners } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat + let bottomStripeIsHidden: Bool switch neighbors.bottom { case .sameSection(false): bottomStripeInset = leftInset + editingOffset bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false + bottomStripeIsHidden = false default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners + bottomStripeIsHidden = hasCorners } + strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil @@ -362,10 +370,9 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL strongSelf.avatarNode.image = updatedAvatarImage } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) + strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition) strongSelf.backgroundNode.isHidden = false - strongSelf.highlightedBackgroundNode.isHidden = true strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) @@ -379,39 +386,8 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } + self.isHighlighted = highlighted + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) } override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { @@ -446,6 +422,12 @@ class ChatListFilterPresetCategoryItemNode: ItemListRevealOptionsItemNode, ItemL transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + editingOffset + params.leftInset + 15.0, y: self.avatarNode.frame.minY), size: self.avatarNode.bounds.size)) } + + override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) + } override func revealOptionsInteractivelyOpened() { if let item = self.item { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 3d58d6a74c..54e099ffe6 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -2071,11 +2071,16 @@ public func chatListFilterPresetController(context: AccountContext, currentPrese ) |> deliverOnMainQueue |> map { presentationData, stateWithPeers, peerView, premiumLimits, sharedLinks, currentPreset -> (ItemListControllerState, (ItemListNodeState, Any)) in + var presentationData = presentationData + + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + let (state, includePeers, excludePeers) = stateWithPeers let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false - let leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { if let attemptNavigationImpl { attemptNavigationImpl({ value in if value { @@ -2086,7 +2091,7 @@ public func chatListFilterPresetController(context: AccountContext, currentPrese dismissImpl?() } }) - let rightNavigationButton = ItemListNavigationButton(content: .text("___done"), style: .bold, enabled: state.isComplete, action: { + let rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: state.isComplete, action: { applyImpl?(false, { dismissImpl?() }) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 0d90835b6f..f4817f217f 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -613,6 +613,11 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch ) ) |> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits, displayTags -> (ItemListControllerState, (ItemListNodeState, Any)) in + var presentationData = presentationData + + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + let isPremium = peer?.isPremium ?? false let limits = allLimits.0 let premiumLimits = allLimits.1 @@ -622,13 +627,13 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch case .default: leftNavigationButton = nil case .modal: - leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) } let rightNavigationButton: ItemListNavigationButton? if state.isEditing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { let _ = (updatedFilterOrder.get() |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak updatedFilterOrder] updatedFilterOrderValue in diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 84e5395075..37cba290b4 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -16,7 +16,7 @@ struct ChatListFilterPresetListItemEditing: Equatable { let revealed: Bool } -final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { +final class ChatListFilterPresetListItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem { let context: AccountContext let presentationData: ItemListPresentationData let systemStyle: ItemListSystemStyle @@ -33,6 +33,10 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { let action: () -> Void let setItemWithRevealedOptions: (Int32?, Int32?) -> Void let remove: () -> Void + + var hasActiveRevealOptions: Bool { + return self.editing.revealed + } init( context: AccountContext, @@ -145,6 +149,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { private var item: ChatListFilterPresetListItem? private var layoutParams: ListViewItemLayoutParams? + private var isHighlighted = false override var canBeSelected: Bool { if self.editableControlNode != nil { @@ -242,7 +247,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { let peerRevealOptions: [ItemListRevealOption] if item.editing.editable && item.canBeDeleted { - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)] } else { peerRevealOptions = [] } @@ -396,26 +401,29 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false + let topStripeIsHidden: Bool switch neighbors.top { case .sameSection(false): - strongSelf.topStripeNode.isHidden = true + topStripeIsHidden = true default: hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners + topStripeIsHidden = hasCorners } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat + let bottomStripeIsHidden: Bool switch neighbors.bottom { case .sameSection(false): bottomStripeInset = leftInset + editingOffset bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false + bottomStripeIsHidden = false default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners + bottomStripeIsHidden = hasCorners } + strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil @@ -425,9 +433,9 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 1.0), size: titleLayout.size)) - let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset), size: labelLayout.size) + let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset + 1.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0) @@ -496,7 +504,7 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) + strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition) strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) @@ -517,39 +525,8 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } + self.isHighlighted = highlighted + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) } override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { @@ -605,6 +582,12 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(view: tagIconView, frame: tagIconFrame) } } + + override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) + } override func revealOptionsInteractivelyOpened() { if let item = self.item { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift index 4cce378d6a..a2aa4bccb7 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListSuggestedItem.swift @@ -215,14 +215,8 @@ public class ChatListFilterPresetListSuggestedItemNode: ListViewItemNode, ItemLi let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor:labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let verticalInset: CGFloat - switch item.systemStyle { - case .glass: - verticalInset = 15.0 - case .legacy: - verticalInset = 11.0 - } - let titleSpacing: CGFloat = 3.0 + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 2.0 let height: CGFloat height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 9a5b05d768..65b39eb784 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -224,7 +224,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo context.engine.accountData.addAppLogEvent(type: "search_global_open_message", data: .dictionary(["msg_id": .number(Double(messageId.id))])) } }, openUrl: { [weak self] url in - let _ = openUserGeneratedUrl(context: context, peerId: nil, url: url, concealed: false, present: { c in + let _ = context.sharedContext.openUserGeneratedUrl(context: context, peerId: nil, url: url, webpage: nil, concealed: false, forceConcealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: false, present: { c in present(c, nil) }, openResolved: { [weak self] resolved in context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peerId, navigation in @@ -240,7 +240,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo }, dismissInput: { self?.dismissInput() }, contentContext: nil, progress: nil, completion: nil) - }) + }, progress: nil, alertDisplayUpdated: nil, concealedAlertOption: nil) }, clearRecentSearch: { [weak self] in guard let strongSelf = self else { return diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 0b47c1eacf..498ebd2303 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3517,6 +3517,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in + }, performBotConnectionReviewAction: { _, _ in }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { [weak self] subject, sourceNode in @@ -5814,6 +5815,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in + }, performBotConnectionReviewAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, openStarsTopup: { _ in @@ -6540,10 +6542,10 @@ private final class EmptyResultsButton: Component { 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), - cornerRadius: 10.0 + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: buttonContent, isEnabled: isEnabled, @@ -6555,7 +6557,7 @@ private final class EmptyResultsButton: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width, height: 50.0) + containerSize: CGSize(width: availableSize.width, height: 52.0) ) if let buttonView = self.button.view { if buttonView.superview == nil { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 68b09cf72a..1189898132 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -156,7 +156,7 @@ public final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, performBotConnectionReviewAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in }, editPeer: { _ in }, openWebApp: { _ in }, openPhotoSetup: { diff --git a/submodules/ChatListUI/Sources/Node/ChatListArchiveInfoItem.swift b/submodules/ChatListUI/Sources/Node/ChatListArchiveInfoItem.swift index 411cd295b9..4abb01fb48 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListArchiveInfoItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListArchiveInfoItem.swift @@ -185,6 +185,7 @@ class ChatListArchiveInfoItemNode: ListViewItemNode, ASScrollViewDelegate { self.view.disablesInteractiveTransitionGestureRecognizer = true + self.scrollNode.view.scrollsToTop = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 7cb442d48e..62ec39b3c2 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -529,10 +529,10 @@ public class ChatListItem: ListViewItem { public 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 = ChatListItemNode() - let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) + let (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) node.insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) - let (nodeLayout, apply) = node.asyncLayout()(self, params, first, last, firstWithHeader, nextIsPinned) + let (nodeLayout, apply) = node.asyncLayout()(self, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) node.insets = nodeLayout.insets node.contentSize = nodeLayout.contentSize @@ -556,13 +556,13 @@ public class ChatListItem: ListViewItem { nodeValue.setupItem(item: self, synchronousLoads: false) let layout = nodeValue.asyncLayout() async { - let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) + let (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) var animated = true if case .None = animation { animated = false } - let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, nextIsPinned) + let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) Queue.mainQueue().async { completion(nodeLayout, { _ in apply(false, animated) @@ -600,7 +600,7 @@ public class ChatListItem: ListViewItem { } } - static func mergeType(item: ChatListItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool) { + static func mergeType(item: ChatListItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, nextHasActiveRevealControls: Bool) { var first = false var last = false var firstWithHeader = false @@ -617,14 +617,16 @@ public class ChatListItem: ListViewItem { firstWithHeader = item.header != nil } var nextIsPinned = false + var nextHasActiveRevealControls = false if let nextItem = nextItem as? ChatListItem { if case let .chatList(nextIndex) = nextItem.index, nextIndex.pinningIndex != nil { nextIsPinned = true } + nextHasActiveRevealControls = nextItem.hasActiveRevealControls } else { last = true } - return (first, last, firstWithHeader, nextIsPinned) + return (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) } } @@ -638,9 +640,9 @@ private let ungroupIcon = ItemListRevealOptionIcon.animation(animation: "anim_un private let readIcon = ItemListRevealOptionIcon.animation(animation: "anim_read", scale: 1.0, offset: 0.0, replaceColors: nil, flip: false) private let unreadIcon = ItemListRevealOptionIcon.animation(animation: "anim_unread", scale: 1.0, offset: 0.0, replaceColors: [0x2194fa], flip: false) private let archiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_archive", scale: 1.0, offset: 2.0, replaceColors: [0xa9a9ad], flip: false) -private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.642, offset: -9.0, replaceColors: [0xa9a9ad], flip: false) -private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: 2.0, replaceColors: [0xbdbdc2], flip: false) -private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -20.0, replaceColors: [0xbdbdc2], flip: true) +private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.52, offset: -6.0, replaceColors: [0xa9a9ad], flip: false) +private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.1, offset: 2.0, replaceColors: [0xbdbdc2], flip: false) +private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -15.0, replaceColors: [0xbdbdc2], flip: true) private let startIcon = ItemListRevealOptionIcon.animation(animation: "anim_play", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false) private let closeIcon = ItemListRevealOptionIcon.animation(animation: "anim_pause", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false) @@ -686,28 +688,28 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem if !isEditing { if case .savedMessagesChats = location { if isPinned { - options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { - options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } } else if case .chatList(.archive) = location { if isPinned { - options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { - options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } } else { if let isMuted = isMuted { if isMuted { - options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { - options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor)) } } } } if canDelete { - options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, iconColor: theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } if case .savedMessagesChats = location { } else { @@ -729,10 +731,10 @@ private func revealOptions(strings: PresentationStrings, theme: PresentationThem } if canArchive { if canArchivePeer(id: peerId, accountPeerId: accountPeerId) { - options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.archive.rawValue, title: strings.ChatList_ArchiveAction, icon: archiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.inactive.foregroundColor, textColor: theme.chatList.dateTextColor)) } } else if canUnarchive { - options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.unarchive.rawValue, title: strings.ChatList_UnarchiveAction, icon: unarchiveIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.inactive.foregroundColor, textColor: theme.chatList.dateTextColor)) } } } @@ -743,9 +745,9 @@ private func groupReferenceRevealOptions(strings: PresentationStrings, theme: Pr var options: [ItemListRevealOption] = [] if !isEditing { if hiddenByDefault { - options.append(ItemListRevealOption(key: RevealOptionKey.unhide.rawValue, title: strings.ChatList_UnhideAction, icon: unhideIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.unhide.rawValue, title: strings.ChatList_UnhideAction, icon: unhideIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { - options.append(ItemListRevealOption(key: RevealOptionKey.hide.rawValue, title: strings.ChatList_HideAction, icon: hideIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.hide.rawValue, title: strings.ChatList_HideAction, icon: hideIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: theme.chatList.dateTextColor)) } } return options @@ -756,9 +758,9 @@ private func forumGeneralRevealOptions(strings: PresentationStrings, theme: Pres if !isEditing { if let isMuted = isMuted { if isMuted { - options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { - options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor)) } } } @@ -767,16 +769,16 @@ private func forumGeneralRevealOptions(strings: PresentationStrings, theme: Pres if !isClosed { } else { - options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: startIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: startIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } } } if canHide { if !isEditing { if hiddenByDefault { - options.append(ItemListRevealOption(key: RevealOptionKey.unhide.rawValue, title: strings.ChatList_ThreadUnhideAction, icon: unhideIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.unhide.rawValue, title: strings.ChatList_ThreadUnhideAction, icon: unhideIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { - options.append(ItemListRevealOption(key: RevealOptionKey.hide.rawValue, title: strings.ChatList_ThreadHideAction, icon: hideIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.hide.rawValue, title: strings.ChatList_ThreadHideAction, icon: hideIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: theme.chatList.dateTextColor)) } } } @@ -788,21 +790,21 @@ private func forumThreadRevealOptions(strings: PresentationStrings, theme: Prese if !isEditing { if let isMuted = isMuted { if isMuted { - options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { - options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: theme.chatList.dateTextColor)) } } } if canDelete { - options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, iconColor: theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } if canOpenClose { if !isEditing { if !isClosed { - options.append(ItemListRevealOption(key: RevealOptionKey.close.rawValue, title: strings.ChatList_CloseAction, icon: closeIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.inactive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.close.rawValue, title: strings.ChatList_CloseAction, icon: closeIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.inactive.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { - options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: startIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.open.rawValue, title: strings.ChatList_StartAction, icon: startIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } } } @@ -815,7 +817,7 @@ private func leftRevealOptions(strings: PresentationStrings, theme: Presentation if case .root = groupId { var options: [ItemListRevealOption] = [] if isUnread { - options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Read, icon: readIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Read, icon: readIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, iconColor: theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { var canMarkUnread = true if case let .channel(channel) = peer, channel.isForumOrMonoForum { @@ -823,15 +825,15 @@ private func leftRevealOptions(strings: PresentationStrings, theme: Presentation } if canMarkUnread { - options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Unread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Unread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, iconColor: theme.list.itemDisclosureActions.accent.foregroundColor, textColor: theme.chatList.dateTextColor)) } } if !isEditing { if isPinned { - options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } else { if filterData == nil || peer.id.namespace != Namespaces.Peer.SecretChat { - options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor)) + options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, iconColor: theme.list.itemDisclosureActions.constructive.foregroundColor, textColor: theme.chatList.dateTextColor)) } } } @@ -1387,9 +1389,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { private var cachedChatListQuoteSearchResult: CachedChatListSearchResult? private var cachedCustomTextEntities: CachedCustomTextEntities? - var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, ListViewItemLayoutParams, countersSize: CGFloat)? + var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, nextHasActiveRevealControls: Bool, ListViewItemLayoutParams, countersSize: CGFloat)? private var isHighlighted: Bool = false + private var nextHasActiveRevealControls: Bool = false private var skipFadeout: Bool = false private var customAnimationInProgress: Bool = false @@ -1686,7 +1689,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in if let strongSelf = self, let layoutParams = strongSelf.layoutParams { - let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.5, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4) + let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.6, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4, layoutParams.5) let _ = apply(false, false) } }) @@ -2036,8 +2039,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { let layout = self.asyncLayout() - let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem) - let (nodeLayout, apply) = layout(item as! ChatListItem, params, first, last, firstWithHeader, nextIsPinned) + let (first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem) + let (nodeLayout, apply) = layout(item as! ChatListItem, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls) apply(false, false) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets @@ -2056,7 +2059,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } var reallyHighlighted: Bool { - var reallyHighlighted = self.isHighlighted + var reallyHighlighted = self.isHighlighted || self.isRevealOptionsActive if let item = self.item { if let itemChatLocation = item.content.chatLocation { if itemChatLocation == item.interaction.highlightedChatLocation?.location { @@ -2074,6 +2077,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { func updateIsHighlighted(transition: ContainedViewLayoutTransition) { let highlightProgress: CGFloat = self.item?.interaction.highlightedChatLocation?.progress ?? 1.0 + transition.updateCornerRadius(node: self.highlightedBackgroundNode, cornerRadius: self.isRevealOptionsActive ? 26.0 : 0.0) + self.updateSeparatorAlpha(transition: transition) if self.reallyHighlighted { if self.highlightedBackgroundNode.supernode == nil { @@ -2136,6 +2141,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } } + + private func updateSeparatorAlpha(transition: ContainedViewLayoutTransition, inlineNavigationProgress: CGFloat? = nil) { + let revealSeparatorAlpha: CGFloat = (self.isRevealOptionsActive || self.isNextRevealOptionsActive || self.nextHasActiveRevealControls) ? 0.0 : 1.0 + if let inlineNavigationProgress = inlineNavigationProgress ?? self.item?.interaction.inlineNavigationLocation?.progress { + transition.updateAlpha(node: self.separatorNode, alpha: (1.0 - inlineNavigationProgress) * revealSeparatorAlpha) + } else { + transition.updateAlpha(node: self.separatorNode, alpha: revealSeparatorAlpha) + } + } override public func tapped() { guard let item = self.item, item.editing else { @@ -2153,7 +2167,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } - func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { + func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool, _ nextHasActiveRevealControls: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) let makeTrailingTextBadgeLayout = TextNode.asyncLayout(self.trailingTextBadgeNode) @@ -2175,8 +2189,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let currentChatListQuoteSearchResult = self.cachedChatListQuoteSearchResult let currentCustomTextEntities = self.cachedCustomTextEntities - return { item, params, first, last, firstWithHeader, nextIsPinned in - let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) + return { item, params, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls in + let titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) let italicTextFont = Font.italic(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) @@ -2450,7 +2464,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } else if !useChatListLayout { avatarLeftInset = 50.0 } else { - avatarLeftInset = 18.0 + avatarDiameter + avatarLeftInset = 24.0 + avatarDiameter } } @@ -2589,7 +2603,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { isUser = true } var isGuestChatAuthor = false - if case let .user(user) = messages.last?.author, let botInfo = user.botInfo, botInfo.flags.contains(.isGuestChat) { + if let message = messages.last, case let .user(user) = message.author, user.id != message.id.peerId, let botInfo = user.botInfo, botInfo.flags.contains(.isGuestChat) { isGuestChatAuthor = true } @@ -3503,7 +3517,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let layoutOffset: CGFloat = 0.0 - let rawContentWidth = params.width - leftInset - params.rightInset - 10.0 - editingOffset + let rawContentWidth = params.width - leftInset - params.rightInset - 18.0 - editingOffset let (dateLayout, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -3512,7 +3526,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentMentionBadgeImage, mentionBadgeContent) var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)? - if case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { + if !item.editing, case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.ChatList_InlineButtonOpenApp, font: Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) } @@ -3640,7 +3654,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { textMaxWidth -= 18.0 } - let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: (authorAttributedString == nil && itemTags.isEmpty && forumThread == nil && topForumTopicItems.isEmpty) ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: textMaxWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: textCutout, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments( + attributedString: textAttributedString, + backgroundColor: nil, + maximumNumberOfLines: (authorAttributedString == nil && itemTags.isEmpty && forumThread == nil && topForumTopicItems.isEmpty) ? 2 : 1, + truncationType: .end, + constrainedSize: CGSize(width: textMaxWidth, height: .greatestFiniteMagnitude), + alignment: .natural, + lineSpacing: 0.2, + cutout: textCutout, + insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0) + )) let maxTitleLines: Int switch item.index { @@ -3770,15 +3794,15 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } } else if case .psa = promoInfo { peerRevealOptions = [ - ItemListRevealOption(key: RevealOptionKey.hidePsa.rawValue, title: item.presentationData.strings.ChatList_HideAction, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor) + ItemListRevealOption(key: RevealOptionKey.hidePsa.rawValue, title: item.presentationData.strings.ChatList_HideAction, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor) ] peerLeftRevealOptions = [] } else if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { peerLeftRevealOptions = [] if customMessageListData.commandPrefix != nil { peerRevealOptions = [ - ItemListRevealOption(key: RevealOptionKey.edit.rawValue, title: item.presentationData.strings.ChatList_ItemMenuEdit, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.neutral2.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral2.foregroundColor), - ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.ChatList_ItemMenuDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor) + ItemListRevealOption(key: RevealOptionKey.edit.rawValue, title: item.presentationData.strings.ChatList_ItemMenuEdit, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.neutral2.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.neutral2.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor), + ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.ChatList_ItemMenuDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor) ] } else { peerRevealOptions = [] @@ -3800,13 +3824,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if actions.contains(.toggleUnread) { if unreadCount.unread { - peerLeftRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: item.presentationData.strings.DialogList_Read, icon: readIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor)) + peerLeftRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: item.presentationData.strings.DialogList_Read, icon: readIcon, color: item.presentationData.theme.list.itemDisclosureActions.inactive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.neutral1.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor)) } else { - peerLeftRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: item.presentationData.strings.DialogList_Unread, icon: unreadIcon, color: item.presentationData.theme.list.itemDisclosureActions.accent.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor)) + peerLeftRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: item.presentationData.strings.DialogList_Unread, icon: unreadIcon, color: item.presentationData.theme.list.itemDisclosureActions.accent.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor)) } } if actions.contains(.delete) { - peerRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)) + peerRevealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: deleteIcon, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.chatList.dateTextColor)) } } } else { @@ -3864,7 +3888,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { return (layout, { [weak self] synchronousLoads, animated in if let strongSelf = self { - strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned, params, countersSize) + strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned, nextHasActiveRevealControls, params, countersSize) + strongSelf.nextHasActiveRevealControls = nextHasActiveRevealControls strongSelf.currentItemHeight = itemHeight strongSelf.cachedChatListText = chatListText strongSelf.cachedChatListSearchResult = chatListSearchResult @@ -3998,7 +4023,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0) - let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + 6.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) var avatarScaleOffset: CGFloat = 0.0 var avatarScale: CGFloat = 1.0 if let inlineNavigationLocation = item.interaction.inlineNavigationLocation { @@ -4291,7 +4316,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let _ = mentionBadgeApply(animateBadges, true) let _ = onlineApply(animateContent && animateOnline) - var dateFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size) + var dateFrame = CGRect(origin: CGPoint(x: contentRect.maxX - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size) if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData, customMessageListData.messageCount != nil, customMessageListData.commandPrefix == nil { dateFrame.origin.x -= 10.0 @@ -4355,16 +4380,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent) + let rightAccessoryVerticalOffset: CGFloat = -2.0 var nextBadgeX: CGFloat = contentRect.maxX if let _ = currentBadgeBackgroundImage { - let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height) + let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: badgeLayout.width, height: badgeLayout.height) transition.updateFrame(node: strongSelf.badgeNode, frame: badgeFrame) nextBadgeX -= badgeLayout.width + 6.0 } if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil { - let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height) + let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height) transition.updateFrame(node: strongSelf.mentionBadgeNode, frame: badgeFrame) nextBadgeX -= mentionBadgeLayout.width + 6.0 @@ -4375,7 +4401,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.pinnedIconNode.isHidden = false let pinnedIconSize = currentPinnedIconImage.size - let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height) + let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0 + rightAccessoryVerticalOffset, width: pinnedIconSize.width, height: pinnedIconSize.height) strongSelf.pinnedIconNode.frame = pinnedIconFrame nextBadgeX -= pinnedIconSize.width + 6.0 @@ -4392,11 +4418,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + actionButtonSideInset * 2.0, height: actionButtonTitleNodeLayout.size.height + actionButtonTopInset + actionButtonBottomInset) var actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.minY + floor((contentRect.height - actionButtonSize.height) * 0.5), width: actionButtonSize.width, height: actionButtonSize.height) actionButtonFrame.origin.y = max(actionButtonFrame.origin.y, dateFrame.maxY + floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0)) + actionButtonFrame.origin.y += 4.0 let actionButtonNode: HighlightableButtonNode + var animateActionButtonIn = false if let current = strongSelf.actionButtonNode { actionButtonNode = current } else { + animateActionButtonIn = true actionButtonNode = HighlightableButtonNode() strongSelf.actionButtonNode = actionButtonNode strongSelf.mainContentContainerNode.addSubnode(actionButtonNode) @@ -4425,23 +4454,40 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { actionButtonNode.addSubnode(actionButtonTitleNode) } + actionButtonNode.isUserInteractionEnabled = true actionButtonNode.frame = actionButtonFrame actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size) actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: actionButtonTopInset), size: actionButtonTitleNodeLayout.size) + if animateActionButtonIn { + actionButtonNode.alpha = 0.0 + } + transition.updateAlpha(node: actionButtonNode, alpha: 1.0) nextBadgeX -= actionButtonSize.width + 6.0 } else { - if let actionButtonTitleNode = strongSelf.actionButtonTitleNode { - actionButtonTitleNode.removeFromSupernode() - strongSelf.actionButtonTitleNode = nil - } - if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView { - actionButtonBackgroundView.removeFromSuperview() - strongSelf.actionButtonBackgroundView = nil - } if let actionButtonNode = strongSelf.actionButtonNode { - actionButtonNode.removeFromSupernode() + let actionButtonTitleNode = strongSelf.actionButtonTitleNode + let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView + actionButtonNode.isUserInteractionEnabled = false + + strongSelf.actionButtonTitleNode = nil + strongSelf.actionButtonBackgroundView = nil strongSelf.actionButtonNode = nil + + transition.updateAlpha(node: actionButtonNode, alpha: 0.0, completion: { [weak actionButtonNode, weak actionButtonTitleNode, weak actionButtonBackgroundView] _ in + actionButtonTitleNode?.removeFromSupernode() + actionButtonBackgroundView?.removeFromSuperview() + actionButtonNode?.removeFromSupernode() + }) + } else { + if let actionButtonTitleNode = strongSelf.actionButtonTitleNode { + actionButtonTitleNode.removeFromSupernode() + strongSelf.actionButtonTitleNode = nil + } + if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView { + actionButtonBackgroundView.removeFromSuperview() + strongSelf.actionButtonBackgroundView = nil + } } } @@ -4470,9 +4516,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let titleFrame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame - let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height), size: authorLayout) + let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 2.0), size: authorLayout) strongSelf.authorNode.frame = authorNodeFrame - let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.height.isZero ? 0.0 : (authorLayout.height - 3.0))), size: textLayout.size) + let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.minY + titleLayout.size.height - 2.0 + (authorLayout.height.isZero ? 0.0 : (authorLayout.height - 3.0))), size: textLayout.size) if let topForumTopicRect, !isSearching { let compoundHighlightingNode: LinkHighlightingNode @@ -5063,20 +5109,24 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { titleBadge.textNode.removeFromSupernode() } - let separatorInset: CGFloat + let leftSeparatorInset: CGFloat + let rightSeparatorInset: CGFloat if case let .groupReference(groupReferenceData) = item.content, groupReferenceData.hiddenByDefault { - separatorInset = 0.0 + leftSeparatorInset = 0.0 + rightSeparatorInset = 0.0 } else if (!nextIsPinned && isPinned) || last { - separatorInset = 0.0 + leftSeparatorInset = 0.0 + rightSeparatorInset = 0.0 } else { - separatorInset = editingOffset + leftInset + rawContentRect.origin.x + leftSeparatorInset = editingOffset + leftInset + rawContentRect.origin.x + rightSeparatorInset = 16.0 } - transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: layoutOffset + itemHeight - separatorHeight), size: CGSize(width: params.width - separatorInset, height: separatorHeight))) + transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftSeparatorInset, y: layoutOffset + itemHeight - separatorHeight), size: CGSize(width: params.width - leftSeparatorInset - rightSeparatorInset, height: separatorHeight))) if let inlineNavigationLocation = item.interaction.inlineNavigationLocation { - transition.updateAlpha(node: strongSelf.separatorNode, alpha: 1.0 - inlineNavigationLocation.progress) + strongSelf.updateSeparatorAlpha(transition: transition, inlineNavigationProgress: inlineNavigationLocation.progress) } else { - transition.updateAlpha(node: strongSelf.separatorNode, alpha: 1.0) + strongSelf.updateSeparatorAlpha(transition: transition) } if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData { @@ -5122,7 +5172,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.highlightedBackgroundNode.backgroundColor = highlightedBackgroundColor let topNegativeInset: CGFloat = 0.0 - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: layoutOffset - separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: strongSelf.revealOffset, y: layoutOffset - separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset)) + transition.updateCornerRadius(node: strongSelf.highlightedBackgroundNode, cornerRadius: strongSelf.isRevealOptionsActive ? 26.0 : 0.0) if let peerPresence = peerPresence { strongSelf.peerPresenceManager?.reset(presence: EnginePeer.Presence(status: peerPresence.status, lastActivity: 0), isOnline: online) @@ -5266,8 +5317,23 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - + transition.updateBounds(node: self.contextContainer, bounds: self.contextContainer.frame.offsetBy(dx: -offset, dy: 0.0)) + + let highlightedBackgroundFrame = self.highlightedBackgroundNode.frame + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: offset, y: highlightedBackgroundFrame.minY), size: highlightedBackgroundFrame.size)) + } + + override public func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateIsHighlighted(transition: transition) + } + + override public func nextRevealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.nextRevealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateSeparatorAlpha(transition: transition) } override public func touchesToOtherItemsPrevented() { @@ -5471,7 +5537,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) { if let action = action as? ChatListItemAccessibilityCustomAction { - self.revealOptionSelected(ItemListRevealOption(key: action.key, title: "", icon: .none, color: .black, textColor: .white), animated: false) + self.revealOptionSelected(ItemListRevealOption(key: action.key, title: "", icon: .none, color: .black, iconColor: .white, textColor: .white), animated: false) } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index a835020531..2dc44bb69b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -106,6 +106,7 @@ public final class ChatListNodeInteraction { let openActiveSessions: () -> Void let openBirthdaySetup: () -> Void let performActiveSessionAction: (NewSessionReview, Bool) -> Void + let performBotConnectionReviewAction: (NewBotConnectionReview, Bool) -> Void let openChatFolderUpdates: () -> Void let hideChatFolderUpdates: () -> Void let openStories: (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void @@ -166,6 +167,7 @@ public final class ChatListNodeInteraction { openActiveSessions: @escaping () -> Void, openBirthdaySetup: @escaping () -> Void, performActiveSessionAction: @escaping (NewSessionReview, Bool) -> Void, + performBotConnectionReviewAction: @escaping (NewBotConnectionReview, Bool) -> Void, openChatFolderUpdates: @escaping () -> Void, hideChatFolderUpdates: @escaping () -> Void, openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void, @@ -213,6 +215,7 @@ public final class ChatListNodeInteraction { self.openActiveSessions = openActiveSessions self.openBirthdaySetup = openBirthdaySetup self.performActiveSessionAction = performActiveSessionAction + self.performBotConnectionReviewAction = performBotConnectionReviewAction self.openChatFolderUpdates = openChatFolderUpdates self.hideChatFolderUpdates = hideChatFolderUpdates self.openStories = openStories @@ -1796,6 +1799,17 @@ public final class ChatListNode: ListViewImpl { let _ = self.context.engine.privacy.terminateAnotherSession(id: newSessionReview.id).startStandalone() #endif } + }, performBotConnectionReviewAction: { [weak self] newBotConnectionReview, isPositive in + guard let self else { + return + } + + if isPositive { + let _ = self.context.engine.accountData.confirmBotConnectionReview(botId: newBotConnectionReview.botId).startStandalone() + } else { + let _ = removeNewBotConnectionReviews(postbox: self.context.account.postbox, botIds: [newBotConnectionReview.botId]).startStandalone() + let _ = self.context.engine.accountData.setAccountConnectedBot(bot: nil).startStandalone() + } }, openChatFolderUpdates: { [weak self] in guard let self else { return diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 5eea683612..02e3c62d25 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -152,7 +152,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: (EngineMessage.Id?, ChatReplyThreadMessage) -> Void @@ -284,7 +283,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, @@ -416,7 +414,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 @@ -556,7 +553,6 @@ public final class ChatPanelInterfaceInteraction { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { _, _ in }, openScheduledMessages: { - }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { diff --git a/submodules/ChatTextLinkEditUI/BUILD b/submodules/ChatTextLinkEditUI/BUILD index be81825492..a49af0567e 100644 --- a/submodules/ChatTextLinkEditUI/BUILD +++ b/submodules/ChatTextLinkEditUI/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift index 87e357334b..a6e2c31202 100644 --- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift +++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift @@ -10,81 +10,142 @@ import UrlEscaping import ComponentFlow import AlertComponent import AlertMultilineInputFieldComponent +import AlertWebpagePreviewComponent public func chatTextLinkEditController( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, text: String, link: String?, - apply: @escaping (String?) -> Void + preview: Bool = false, + apply: @escaping (String?, TelegramMediaWebpage?) -> Void ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - + let inputState = AlertMultilineInputFieldComponent.ExternalState() - - var content: [AnyComponentWithIdentity] = [] - content.append(AnyComponentWithIdentity( - id: "title", - component: AnyComponent( - AlertTitleComponent(title: link != nil ? strings.TextFormat_EditLinkTitle : strings.TextFormat_AddLinkTitle) - ) - )) - content.append(AnyComponentWithIdentity( - id: "text", - component: AnyComponent( - AlertTextComponent(content: .plain(strings.TextFormat_AddLinkText(text).string)) - ) - )) - + var applyImpl: (() -> Void)? - content.append(AnyComponentWithIdentity( - id: "input", - component: AnyComponent( - AlertMultilineInputFieldComponent( - context: context, - initialValue: link.flatMap { NSAttributedString(string: $0) }, - placeholder: strings.TextFormat_AddLinkPlaceholder, - returnKeyType: .done, - keyboardType: .URL, - autocapitalizationType: .none, - autocorrectionType: .no, - isInitiallyFocused: true, - externalState: inputState, - returnKeyAction: { - applyImpl?() - } - ) - ) - )) - + var effectiveUpdatedPresentationData: (PresentationData, Signal) if let updatedPresentationData { effectiveUpdatedPresentationData = updatedPresentationData } else { effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData) } - + + let makeContent: (TelegramMediaWebpage?) -> [AnyComponentWithIdentity] = { webpage in + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: link != nil ? strings.TextFormat_EditLinkTitle : strings.TextFormat_AddLinkTitle) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(text)) + ) + )) + content.append(AnyComponentWithIdentity( + id: "input", + component: AnyComponent( + AlertMultilineInputFieldComponent( + context: context, + initialValue: link.flatMap { NSAttributedString(string: $0) }, + placeholder: strings.TextFormat_AddLinkPlaceholder, + returnKeyType: .done, + keyboardType: .URL, + autocapitalizationType: .none, + autocorrectionType: .no, + isInitiallyFocused: true, + externalState: inputState, + returnKeyAction: { + applyImpl?() + } + ) + ) + )) + if let webpage { + content.append(AnyComponentWithIdentity( + id: "webpagePreview", + component: AnyComponent(AlertWebpagePreviewComponent( + context: context, + presentationData: effectiveUpdatedPresentationData.0, + webpage: webpage + )) + )) + } + return content + } + + let contentPromise = Promise<[AnyComponentWithIdentity]>() + contentPromise.set(.single(makeContent(nil))) + var dismissImpl: (() -> Void)? let alertController = AlertScreen( configuration: AlertScreen.Configuration(allowInputInset: true), - content: content, - actions: [ + contentSignal: contentPromise.get(), + actionsSignal: .single([ .init(title: strings.Common_Cancel), .init(title: strings.Common_Done, type: .default, action: { applyImpl?() }, autoDismiss: false) - ], + ]), updatedPresentationData: effectiveUpdatedPresentationData ) + + let previewDisposable = MetaDisposable() + var currentPreview: (link: String, webpage: TelegramMediaWebpage)? + if preview { + var currentDisplayedPreview: (link: String, webpage: TelegramMediaWebpage?)? + previewDisposable.set((inputState.valueSignal + |> map { value -> String in + return explicitUrl(value.string) + } + |> distinctUntilChanged + |> mapToSignal { link -> Signal<(String, TelegramMediaWebpage?), NoError> in + guard !link.isEmpty && isValidUrl(link, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) else { + return .single((link, nil)) + } + + let previewSignal = webpagePreview(account: context.account, urls: [link]) + |> map { result -> (String, TelegramMediaWebpage?) in + guard case let .result(result) = result, let webpage = result?.webpage, case .Loaded = webpage.content else { + return (link, nil) + } + return (link, webpage) + } + + return .single((link, nil)) + |> then((.complete() |> delay(1.0, queue: Queue.mainQueue())) |> then(previewSignal)) + } + |> deliverOnMainQueue).startStrict(next: { link, webpage in + if let currentDisplayedPreview, currentDisplayedPreview.link == link && currentDisplayedPreview.webpage == webpage { + return + } + currentDisplayedPreview = (link, webpage) + if let webpage { + currentPreview = (link, webpage) + } else { + currentPreview = nil + } + contentPromise.set(.single(makeContent(webpage))) + })) + } + alertController.dismissed = { _ in + previewDisposable.dispose() + } + applyImpl = { let updatedLink = explicitUrl(inputState.value.string) if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) { dismissImpl?() - apply(updatedLink) + apply(updatedLink, currentPreview?.link == updatedLink ? currentPreview?.webpage : nil) } else if inputState.value.string.isEmpty { dismissImpl?() - apply("") + apply("", nil) } else { inputState.animateError() } diff --git a/submodules/ComponentFlow/Source/Components/Image.swift b/submodules/ComponentFlow/Source/Components/Image.swift index f18aba2e18..3641cf4467 100644 --- a/submodules/ComponentFlow/Source/Components/Image.swift +++ b/submodules/ComponentFlow/Source/Components/Image.swift @@ -7,19 +7,22 @@ public final class Image: Component { public let size: CGSize? public let contentMode: UIImageView.ContentMode public let cornerRadius: CGFloat + public let flipHorizontally: Bool public init( image: UIImage?, tintColor: UIColor? = nil, size: CGSize? = nil, contentMode: UIImageView.ContentMode = .scaleToFill, - cornerRadius: CGFloat = 0.0 + cornerRadius: CGFloat = 0.0, + flipHorizontally: Bool = false ) { self.image = image self.tintColor = tintColor self.size = size self.contentMode = contentMode self.cornerRadius = cornerRadius + self.flipHorizontally = flipHorizontally } public static func ==(lhs: Image, rhs: Image) -> Bool { @@ -38,6 +41,9 @@ public final class Image: Component { if lhs.cornerRadius != rhs.cornerRadius { return false } + if lhs.flipHorizontally != rhs.flipHorizontally { + return false + } return true } @@ -51,7 +57,11 @@ public final class Image: Component { } func update(component: Image, availableSize: CGSize, environment: Environment, transition: ComponentTransition) -> CGSize { - self.image = component.image + if component.flipHorizontally, let cgImage = component.image?.cgImage { + self.image = UIImage(cgImage: cgImage, scale: component.image?.scale ?? 0.0, orientation: .upMirrored) + } else { + self.image = component.image + } self.contentMode = component.contentMode self.clipsToBounds = component.cornerRadius > 0.0 diff --git a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift index c48f851ca9..36cc4388f6 100644 --- a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift +++ b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift @@ -11,15 +11,17 @@ public final class BundleIconComponent: Component { public let scaleFactor: CGFloat public let shadowColor: UIColor? public let shadowBlur: CGFloat + public let flipHorizontally: Bool public let flipVertically: Bool - public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipVertically: Bool = false) { + public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipHorizontally: Bool = false, flipVertically: Bool = false) { self.name = name self.tintColor = tintColor self.maxSize = maxSize self.scaleFactor = scaleFactor self.shadowColor = shadowColor self.shadowBlur = shadowBlur + self.flipHorizontally = flipHorizontally self.flipVertically = flipVertically } @@ -42,6 +44,9 @@ public final class BundleIconComponent: Component { if lhs.shadowBlur != rhs.shadowBlur { return false } + if lhs.flipHorizontally != rhs.flipHorizontally { + return false + } if lhs.flipVertically != rhs.flipVertically { return false } @@ -77,7 +82,9 @@ public final class BundleIconComponent: Component { } }) } - if component.flipVertically, let cgImage = image?.cgImage { + if component.flipHorizontally, let cgImage = image?.cgImage { + self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .upMirrored) + } else if component.flipVertically, let cgImage = image?.cgImage { self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .down) } else { self.image = image diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 980ca8e473..7d0c4b173f 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -291,6 +291,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true + self.scrollNode.view.scrollsToTop = false self.itemNodes = reactions.map { reaction, count in return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, animationCache: animationCache, animationRenderer: animationRenderer, count: count) @@ -897,6 +898,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.clipsToBounds = false + self.scrollNode.view.scrollsToTop = false super.init() 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/BUILD b/submodules/ContactListUI/BUILD index fa3d0c4468..ff00a21cb1 100644 --- a/submodules/ContactListUI/BUILD +++ b/submodules/ContactListUI/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/ContextUI", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/SearchInputPanelComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/SearchBarNode", ], 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/ContactListUI/Sources/InviteContactsCountPanelNode.swift b/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift index 021c4acf53..af68a8ea35 100644 --- a/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift +++ b/submodules/ContactListUI/Sources/InviteContactsCountPanelNode.swift @@ -5,6 +5,7 @@ import Display import TelegramPresentationData import ComponentFlow import ButtonComponent +import AnimatedTextComponent import EdgeEffect final class InviteContactsCountPanelNode: ASDisplayNode { @@ -69,15 +70,11 @@ final class InviteContactsCountPanelNode: ASDisplayNode { ), content: AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(ButtonTextContentComponent( - text: self.strings.Contacts_InviteContacts(Int32(self.count)), - badge: 0, - textColor: self.theme.list.itemCheckColors.foregroundColor, - badgeBackground: self.theme.list.itemCheckColors.foregroundColor, - badgeForeground: self.theme.list.itemCheckColors.fillColor, - badgeStyle: .roundedRectangle, - badgeIconName: nil, - combinedAlignment: true + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 17.0, weight: .semibold, traits: .monospacedNumbers), + color: self.theme.list.itemCheckColors.foregroundColor, + items: [AnimatedTextComponent.Item(id: "text", content: .text(self.strings.Contacts_InviteContacts(Int32(max(1, self.count)))))], + noDelay: true )) ), isEnabled: true, diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index a6a17f1b19..2ba7c55eb9 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -1236,7 +1236,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { color = item.presentationData.theme.list.itemDisclosureActions.accent.fillColor textColor = item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor } - mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor)) + mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, iconColor: textColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)) index += 1 } peerRevealOptions = mappedOptions @@ -2002,7 +2002,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) if item.editing.editable { - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)])) strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) } else { strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) 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/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 3fa40d32dc..49e543eae8 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -1709,7 +1709,7 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun var leftNavigationButton: ItemListNavigationButton? if modal { - leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) } diff --git a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift index a3fcbc3fad..15288011d5 100644 --- a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift +++ b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift @@ -104,19 +104,19 @@ public final class DeviceLocationManager: NSObject { self.currentTopMode = topMode if let topMode = topMode { self.log?("setting mode \(topMode)") + switch topMode { + case .preciseForeground: + self.manager.allowsBackgroundLocationUpdates = false + case .preciseAlways: + self.manager.allowsBackgroundLocationUpdates = true + } + if previousTopMode == nil { if !self.requestedAuthorization { self.requestedAuthorization = true self.manager.requestAlwaysAuthorization() } - - switch topMode { - case .preciseForeground: - self.manager.allowsBackgroundLocationUpdates = false - case .preciseAlways: - self.manager.allowsBackgroundLocationUpdates = true - } - + self.manager.startUpdatingLocation() self.manager.startUpdatingHeading() } diff --git a/submodules/Display/Source/ActionSheetControllerNode.swift b/submodules/Display/Source/ActionSheetControllerNode.swift index c27cc399f8..fc4d61c752 100644 --- a/submodules/Display/Source/ActionSheetControllerNode.swift +++ b/submodules/Display/Source/ActionSheetControllerNode.swift @@ -88,6 +88,12 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate { } } + override func didLoad() { + super.didLoad() + + self.scrollNode.view.scrollsToTop = false + } + func performHighlightedAction() { self.itemGroupsContainerNode.performHighlightedAction() } diff --git a/submodules/Display/Source/ActionSheetItemGroupNode.swift b/submodules/Display/Source/ActionSheetItemGroupNode.swift index cee228963b..af9d79fc27 100644 --- a/submodules/Display/Source/ActionSheetItemGroupNode.swift +++ b/submodules/Display/Source/ActionSheetItemGroupNode.swift @@ -52,6 +52,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.canCancelContentTouches = true self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false super.init() diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index cd2a2a5198..613de4eaf7 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -72,8 +72,21 @@ public extension CALayer { func makeAnimation(from: Any?, to: Any, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation { if timingFunction.hasPrefix(kCAMediaTimingFunctionCustomSpringPrefix) { let components = timingFunction.components(separatedBy: "_") - let damping = Float(components[1]) ?? 100.0 - let initialVelocity = Float(components[2]) ?? 0.0 + let mass: Float + let stiffness: Float + let damping: Float + let initialVelocity: Float + if components.count >= 5 { + mass = Float(components[1]) ?? 5.0 + stiffness = Float(components[2]) ?? 900.0 + damping = Float(components[3]) ?? 100.0 + initialVelocity = Float(components[4]) ?? 0.0 + } else { + mass = 5.0 + stiffness = 900.0 + damping = components.count > 1 ? (Float(components[1]) ?? 100.0) : 100.0 + initialVelocity = components.count > 2 ? (Float(components[2]) ?? 0.0) : 0.0 + } let animation = CASpringAnimation(keyPath: keyPath) animation.fromValue = from @@ -83,10 +96,10 @@ public extension CALayer { if let completion = completion { animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion) } + animation.mass = CGFloat(mass) + animation.stiffness = CGFloat(stiffness) animation.damping = CGFloat(damping) animation.initialVelocity = CGFloat(initialVelocity) - animation.mass = 5.0 - animation.stiffness = 900.0 animation.duration = animation.settlingDuration animation.timingFunction = CAMediaTimingFunction.init(name: .linear) let k = Float(UIView.animationDurationFactor()) diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 17abeb224c..ef5b78b06b 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -14,7 +14,7 @@ public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable { case easeInOut case easeIn case spring - case customSpring(damping: CGFloat, initialVelocity: CGFloat) + case customSpring(mass: CGFloat = 5.0, stiffness: CGFloat = 900.0, damping: CGFloat, initialVelocity: CGFloat) case custom(Float, Float, Float, Float) public static var slide: ContainedViewLayoutTransitionCurve { @@ -52,8 +52,8 @@ public extension ContainedViewLayoutTransitionCurve { return CAMediaTimingFunctionName.easeIn.rawValue case .spring: return kCAMediaTimingFunctionSpring - case let .customSpring(damping, initialVelocity): - return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(damping)_\(initialVelocity)" + case let .customSpring(mass, stiffness, damping, initialVelocity): + return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(mass)_\(stiffness)_\(damping)_\(initialVelocity)" case .custom: return CAMediaTimingFunctionName.easeInEaseOut.rawValue } @@ -124,8 +124,8 @@ private extension CALayer { let timingFunction: String let mediaTimingFunction: CAMediaTimingFunction? switch curve { - case .spring: - timingFunction = kCAMediaTimingFunctionSpring + case .spring, .customSpring: + timingFunction = curve.timingFunction mediaTimingFunction = nil default: timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 05cd1827a1..9378f7a4ce 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -509,6 +509,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur self.scroller.contentSize = CGSize(width: 0.0, height: infiniteScrollSize * 2.0) self.scroller.isHidden = true self.scroller.delegate = self.wrappedScrollViewDelegate + self.scroller.scrollsToTop = false self.view.addSubview(self.scroller) self.scroller.panGestureRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(self.scroller.panGestureRecognizer) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index d8a004fcd0..452418ad57 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -172,6 +172,7 @@ open class NavigationController: UINavigationController, ContainableController, private var inCallStatusBar: StatusBar? private var updateInCallStatusBarState: CallStatusBarNode? private var globalScrollToTopNode: ScrollToTopNode? + private var windowScrollToTopProxyViews: WindowScrollToTopProxyViews? private var rootContainer: RootContainer? private var rootModalFrame: NavigationModalFrame? private var modalContainers: [NavigationModalContainer] = [] @@ -331,6 +332,54 @@ open class NavigationController: UINavigationController, ContainableController, } deinit { + self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil) + } + + private func getWindowScrollToTopProxyViews() -> WindowScrollToTopProxyViews { + if let windowScrollToTopProxyViews = self.windowScrollToTopProxyViews { + return windowScrollToTopProxyViews + } + + let windowScrollToTopProxyViews = WindowScrollToTopProxyViews(scrollToTop: { [weak self] subject in + self?.scrollToTop(subject) + }) + self.windowScrollToTopProxyViews = windowScrollToTopProxyViews + return windowScrollToTopProxyViews + } + + private func windowScrollToTopReferenceView(window: UIWindow) -> UIView? { + var view: UIView? = self.view + while let currentView = view { + if currentView.superview === window { + return currentView + } + view = currentView.superview + } + return nil + } + + private func updateWindowScrollToTopProxyViews(layout: ContainerViewLayout) { + guard let window = self.view.window, let rootContainer = self.rootContainer else { + (self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = true + self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil) + return + } + + (self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = false + let referenceView = self.windowScrollToTopReferenceView(window: window) + let proxyViews = self.getWindowScrollToTopProxyViews() + switch rootContainer { + case .flat: + let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0) + let frame = self.view.convert(CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: scrollToTopHeight)), to: window) + proxyViews.update(window: window, mode: .flat(frame: frame), referenceView: referenceView) + case let .split(container): + let frames = container.scrollToTopProxyFrames(layout: layout) + proxyViews.update(window: window, mode: .split( + masterFrame: container.view.convert(frames.master, to: window), + detailFrame: container.view.convert(frames.detail, to: window) + ), referenceView: referenceView) + } } public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations { @@ -498,7 +547,7 @@ open class NavigationController: UINavigationController, ContainableController, } if let globalScrollToTopNode = self.globalScrollToTopNode { - globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1)) + globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0)) } var overlayContainerLayout = layout @@ -1018,8 +1067,6 @@ open class NavigationController: UINavigationController, ContainableController, case let .flat(flatContainer): let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in self?.controllerRemoved(controller) - }, scrollToTop: { [weak self] subject in - self?.scrollToTop(subject) }) if let detailsPlaceholderNode = self.detailsPlaceholderNode { self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode) @@ -1053,8 +1100,6 @@ open class NavigationController: UINavigationController, ContainableController, } else { let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in self?.controllerRemoved(controller) - }, scrollToTop: { [weak self] subject in - self?.scrollToTop(subject) }) if let detailsPlaceholderNode = self.detailsPlaceholderNode { self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode) @@ -1385,6 +1430,8 @@ open class NavigationController: UINavigationController, ContainableController, } } + self.updateWindowScrollToTopProxyViews(layout: layout) + self.isUpdatingContainers = false if notifyGlobalOverlayControllersUpdated { diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index f9120ce6fe..7fd3ed95ba 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -91,6 +91,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.scrollNode.view.clipsToBounds = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.tag = 0x5C4011 + self.scrollNode.view.scrollsToTop = false let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in guard let strongSelf = self, !strongSelf.isDismissed else { diff --git a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift index 42192baf34..b3900d15c5 100644 --- a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift @@ -11,8 +11,6 @@ enum NavigationSplitContainerScrollToTop { final class NavigationSplitContainer: ASDisplayNode { private var theme: NavigationControllerTheme - private let masterScrollToTopView: ScrollToTopView - private let detailScrollToTopView: ScrollToTopView private let masterContainer: NavigationContainer private let detailContainer: NavigationContainer private let separator: ASDisplayNode @@ -39,18 +37,9 @@ final class NavigationSplitContainer: ASDisplayNode { self.detailContainer.isInFocus = isInFocus } - init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void, scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) { + init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void) { self.theme = theme - self.masterScrollToTopView = ScrollToTopView(frame: CGRect()) - self.masterScrollToTopView.action = { - scrollToTop(.master) - } - self.detailScrollToTopView = ScrollToTopView(frame: CGRect()) - self.detailScrollToTopView.action = { - scrollToTop(.detail) - } - self.masterContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved) self.masterContainer.clipsToBounds = true @@ -65,8 +54,6 @@ final class NavigationSplitContainer: ASDisplayNode { self.addSubnode(self.masterContainer) self.addSubnode(self.detailContainer) self.addSubnode(self.separator) - self.view.addSubview(self.masterScrollToTopView) - self.view.addSubview(self.detailScrollToTopView) } func hasNonReadyControllers() -> Bool { @@ -83,13 +70,21 @@ final class NavigationSplitContainer: ASDisplayNode { self.separator.backgroundColor = theme.navigationBar.separatorColor } + func scrollToTopProxyFrames(layout: ContainerViewLayout) -> (master: CGRect, detail: CGRect) { + let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) + let detailWidth = layout.size.width - masterWidth + let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0) + + return ( + master: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: scrollToTopHeight)), + detail: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: scrollToTopHeight)) + ) + } + func update(layout: ContainerViewLayout, masterControllers: [ViewController], detailControllers: [ViewController], detailsPlaceholderNode: NavigationDetailsPlaceholderNode?, transition: ContainedViewLayoutTransition) { let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) let detailWidth = layout.size.width - masterWidth - self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 10.0)) - self.detailScrollToTopView.frame = CGRect(origin: CGPoint(x: masterWidth, y: -1.0), size: CGSize(width: detailWidth, height: 1.0)) - transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height))) transition.updateFrame(node: self.detailContainer, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height))) transition.updateFrame(node: self.separator, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height))) diff --git a/submodules/Display/Source/ScrollToTopProxyView.swift b/submodules/Display/Source/ScrollToTopProxyView.swift index ba727f4d11..ee95519900 100644 --- a/submodules/Display/Source/ScrollToTopProxyView.swift +++ b/submodules/Display/Source/ScrollToTopProxyView.swift @@ -35,10 +35,6 @@ class ScrollToTopView: UIScrollView, UIScrollViewDelegate { return false } - - func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { - print("scrollViewDidScrollToTop") - } } class ScrollToTopNode: ASDisplayNode { @@ -52,3 +48,83 @@ class ScrollToTopNode: ASDisplayNode { }) } } + +final class WindowScrollToTopProxyViews { + enum Mode { + case disabled + case flat(frame: CGRect) + case split(masterFrame: CGRect, detailFrame: CGRect) + } + + private let flatView: ScrollToTopView + private let masterView: ScrollToTopView + private let detailView: ScrollToTopView + + init(scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) { + self.flatView = ScrollToTopView(frame: CGRect()) + self.masterView = ScrollToTopView(frame: CGRect()) + self.detailView = ScrollToTopView(frame: CGRect()) + + self.flatView.action = { + scrollToTop(.master) + } + self.masterView.action = { + scrollToTop(.master) + } + self.detailView.action = { + scrollToTop(.detail) + } + } + + deinit { + self.disable(self.flatView) + self.disable(self.masterView) + self.disable(self.detailView) + } + + func update(window: UIWindow?, mode: Mode, referenceView: UIView?) { + guard let window else { + self.disable(self.flatView) + self.disable(self.masterView) + self.disable(self.detailView) + return + } + + switch mode { + case .disabled: + self.disable(self.flatView) + self.disable(self.masterView) + self.disable(self.detailView) + case let .flat(frame): + self.activate(self.flatView, in: window, frame: frame, referenceView: referenceView) + self.disable(self.masterView) + self.disable(self.detailView) + case let .split(masterFrame, detailFrame): + self.disable(self.flatView) + self.activate(self.masterView, in: window, frame: masterFrame, referenceView: referenceView) + self.activate(self.detailView, in: window, frame: detailFrame, referenceView: referenceView) + } + } + + private func activate(_ view: ScrollToTopView, in window: UIWindow, frame: CGRect, referenceView: UIView?) { + if let referenceView, referenceView.superview === window { + if view.superview !== window { + view.removeFromSuperview() + } + window.insertSubview(view, aboveSubview: referenceView) + } else if view.superview !== window { + view.removeFromSuperview() + window.addSubview(view) + } + + view.isHidden = false + view.scrollsToTop = true + view.frame = frame + } + + private func disable(_ view: ScrollToTopView) { + view.scrollsToTop = false + view.isHidden = true + view.removeFromSuperview() + } +} diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index e95073b967..499389abb4 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -107,6 +107,11 @@ swift_library( "//submodules/TelegramUI/Components/DynamicCornerRadiusView", "//submodules/TelegramUI/Components/StickerPickerScreen", "//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation", + "//submodules/TelegramUI/Components/SegmentControlComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/LiquidLens", + "//submodules/TelegramUI/Components/TabSelectionRecognizer", + "//submodules/TelegramUI/Components/GlassControls", ], visibility = [ "//visibility:public", diff --git a/submodules/DrawingUI/Sources/ColorPickerScreen.swift b/submodules/DrawingUI/Sources/ColorPickerScreen.swift index dfcdf6bef8..206563fa1b 100644 --- a/submodules/DrawingUI/Sources/ColorPickerScreen.swift +++ b/submodules/DrawingUI/Sources/ColorPickerScreen.swift @@ -10,10 +10,12 @@ import TelegramPresentationData import SheetComponent import ViewControllerComponent import BlurredBackgroundComponent -import SegmentedControlNode +import SegmentControlComponent import MultilineTextComponent import HexColor import MediaEditor +import GlassBarButtonComponent +import BundleIconComponent private let palleteColors: [UInt32] = [ 0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000, @@ -1494,73 +1496,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 @@ -1886,30 +1821,6 @@ private final class ColorPickerContent: CombinedComponent { } final class State: ComponentState { - var cachedEyedropperImage: UIImage? - var eyedropperImage: UIImage { - let eyedropperImage: UIImage - if let image = self.cachedEyedropperImage { - eyedropperImage = image - } else { - eyedropperImage = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Eyedropper"), color: .white)! - self.cachedEyedropperImage = eyedropperImage - } - return eyedropperImage - } - - var cachedCloseImage: UIImage? - var closeImage: UIImage { - let closeImage: UIImage - if let image = self.cachedCloseImage { - closeImage = image - } else { - closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xa8aab1))! - self.cachedCloseImage = closeImage - } - return closeImage - } - var selectedMode: Int = 0 var selectedColor: DrawingColor @@ -1951,10 +1862,10 @@ private final class ColorPickerContent: CombinedComponent { } static var body: Body { - let eyedropperButton = Child(Button.self) - let closeButton = Child(Button.self) + let eyedropperButton = Child(GlassBarButtonComponent.self) + let closeButton = Child(GlassBarButtonComponent.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) @@ -1985,50 +1896,45 @@ private final class ColorPickerContent: CombinedComponent { let sideInset: CGFloat = 16.0 let eyedropperButton = eyedropperButton.update( - component: Button( - content: AnyComponent( - Image(image: state.eyedropperImage) + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(BundleIconComponent(name: "Media Editor/Eyedropper", tintColor: .white)) ), - action: { [weak component] in - component?.eyedropper() + action: { _ in + component.eyedropper() } - ).minSize(CGSize(width: 30.0, height: 30.0)), - availableSize: CGSize(width: 19.0, height: 19.0), + ), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(eyedropperButton - .position(CGPoint(x: environment.safeInsets.left + eyedropperButton.size.width + 1.0, y: 29.0)) + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - eyedropperButton.size.width / 2.0 - 16.0, y: 16.0 + eyedropperButton.size.height / 2.0)) ) let closeButton = closeButton.update( - component: Button( - content: AnyComponent(ZStack([ - AnyComponentWithIdentity( - id: "background", - component: AnyComponent( - BlurredBackgroundComponent( - color: UIColor(rgb: 0x888888, alpha: 0.1) - ) - ) - ), - AnyComponentWithIdentity( - id: "icon", - component: AnyComponent( - Image(image: state.closeImage) - ) - ), - ])), - action: { [weak component] in - component?.dismiss() + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(BundleIconComponent(name: "Navigation/Close", tintColor: .white)) + ), + action: { _ in + component.dismiss() } ), availableSize: CGSize(width: 30.0, height: 30.0), transition: .immediate ) context.add(closeButton - .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - closeButton.size.width - 1.0, y: 29.0)) - .clipsToBounds(true) - .cornerRadius(15.0) + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) let title = title.update( @@ -2046,17 +1952,24 @@ private final class ColorPickerContent: CombinedComponent { transition: .immediate ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 29.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 16.0 + 22.0)) ) - var contentHeight: CGFloat = 58.0 + var contentHeight: CGFloat = 76.0 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 +2292,7 @@ private final class ColorPickerSheetComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .blur(.dark), animateOut: animateOut ), diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 2678fbe440..aa578ffa70 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -25,6 +25,9 @@ import FastBlur import MediaEditor import StickerPickerScreen import ImageObjectSeparation +import GlassBarButtonComponent +import GlassControls +import BundleIconComponent public struct DrawingResultData { public let data: Data? @@ -363,15 +366,17 @@ private final class ReferenceContentSource: ContextReferenceContentSource { private let sourceView: UIView private let contentArea: CGRect private let customPosition: CGPoint + private let actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition - init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint) { + init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint, actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition = .top) { self.sourceView = sourceView self.contentArea = contentArea self.customPosition = customPosition + self.actionsPosition = actionsPosition } func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: .top) + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: self.actionsPosition) } } @@ -467,13 +472,11 @@ private let bottomGradientTag = GenericComponentViewTag() private let undoButtonTag = GenericComponentViewTag() private let redoButtonTag = GenericComponentViewTag() private let clearAllButtonTag = GenericComponentViewTag() +private let topButtonsTag = GenericComponentViewTag() private let colorButtonTag = GenericComponentViewTag() private let addButtonTag = GenericComponentViewTag() private let toolsTag = GenericComponentViewTag() private let modeTag = GenericComponentViewTag() -private let flipButtonTag = GenericComponentViewTag() -private let fillButtonTag = GenericComponentViewTag() -private let zoomOutButtonTag = GenericComponentViewTag() private let textSettingsTag = GenericComponentViewTag() private let sizeSliderTag = GenericComponentViewTag() private let fontTag = GenericComponentViewTag() @@ -489,6 +492,12 @@ private let colorTags = [color1Tag, color2Tag, color3Tag, color4Tag, color5Tag, private let cancelButtonTag = GenericComponentViewTag() private let doneButtonTag = GenericComponentViewTag() +enum DrawingMode: Int { + case drawing + case sticker + case text +} + private final class DrawingScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -616,7 +625,6 @@ private final class DrawingScreenComponent: CombinedComponent { final class State: ComponentState { enum ImageKey: Hashable { case undo - case redo case done case add case fill @@ -633,8 +641,6 @@ private final class DrawingScreenComponent: CombinedComponent { switch key { case .undo: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Undo"), color: .white)! - case .redo: - image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Redo"), color: .white)! case .done: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)! case .add: @@ -652,13 +658,7 @@ private final class DrawingScreenComponent: CombinedComponent { return image } } - - enum Mode { - case drawing - case sticker - case text - } - + private let context: AccountContext private let updateToolState: ActionSlot private let insertEntity: ActionSlot @@ -675,7 +675,7 @@ private final class DrawingScreenComponent: CombinedComponent { private let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? private let present: (ViewController) -> Void - var currentMode: Mode + var currentMode: DrawingMode var drawingState: DrawingState var drawingViewState: DrawingView.NavigationState var currentColor: DrawingColor @@ -992,7 +992,7 @@ private final class DrawingScreenComponent: CombinedComponent { self.present(contextController) } - func updateCurrentMode(_ mode: Mode, update: Bool = true) { + func updateCurrentMode(_ mode: DrawingMode, update: Bool = true) { self.currentMode = mode if let selectedEntity = self.selectedEntity { if selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity { @@ -1058,15 +1058,13 @@ private final class DrawingScreenComponent: CombinedComponent { let topGradient = Child(BlurredGradientComponent.self) let bottomGradient = Child(BlurredGradientComponent.self) - let undoButton = Child(Button.self) + let undoButton = Child(GlassBarButtonComponent.self) + let redoButton = Child(GlassBarButtonComponent.self) + let clearAllButton = Child(GlassBarButtonComponent.self) + let topButtons = Child(GlassControlPanelComponent.self) - let redoButton = Child(Button.self) - let clearAllButton = Child(Button.self) - - let zoomOutButton = Child(Button.self) - let tools = Child(ToolsComponent.self) - let modeAndSize = Child(ModeAndSizeComponent.self) + let mode = Child(ModeComponent.self) let colorButton = Child(ColorSwatchComponent.self) @@ -1082,16 +1080,11 @@ private final class DrawingScreenComponent: CombinedComponent { let swatch8Button = Child(ColorSwatchComponent.self) let addButton = Child(Button.self) - - let flipButton = Child(Button.self) - let fillButton = Child(Button.self) - - let backButton = Child(Button.self) - let doneButton = Child(Button.self) + + let backButton = Child(GlassBarButtonComponent.self) + let doneButton = Child(GlassBarButtonComponent.self) let textSize = Child(TextSizeSliderComponent.self) - let textCancelButton = Child(Button.self) - let textDoneButton = Child(Button.self) let presetColors: [DrawingColor] = [ DrawingColor(rgb: 0xff453a), @@ -1220,6 +1213,8 @@ private final class DrawingScreenComponent: CombinedComponent { var additionalBottomInset: CGFloat = 0.0 if component.sourceHint == .storyEditor { additionalBottomInset = max(0.0, previewBottomInset - environment.safeInsets.bottom - 49.0) + } else { + additionalBottomInset = 8.0 } if let textEntity = state.selectedEntity as? DrawingTextEntity { @@ -1318,8 +1313,8 @@ private final class DrawingScreenComponent: CombinedComponent { ) } - let rightButtonPosition = rightEdge - 24.0 - var offsetX: CGFloat = leftEdge + 24.0 + let rightButtonPosition = rightEdge - 28.0 + var offsetX: CGFloat = leftEdge + 28.0 let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0 let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in @@ -1608,6 +1603,7 @@ private final class DrawingScreenComponent: CombinedComponent { } var hasTopButtons = false + var centerItems: [GlassControlGroupComponent.Item] = [] if let entity = state.selectedEntity { var isFilled: Bool? if let entity = entity as? DrawingSimpleShapeEntity { @@ -1625,12 +1621,13 @@ private final class DrawingScreenComponent: CombinedComponent { hasTopButtons = isFilled != nil || hasFlip - if let isFilled = isFilled { - let fillButton = fillButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(isFilled ? .fill : .stroke)) - ), + if controlsAreVisible { + if let isFilled = isFilled { + centerItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("fill"), + content: .customIcon(id: AnyHashable("fill"), component: AnyComponent( + Image(image: state.image(isFilled ? .fill : .stroke), size: CGSize(width: 30.0, height: 30.0)) + ), insets: .zero), action: { [weak state] in guard let state = state else { return @@ -1661,25 +1658,15 @@ private final class DrawingScreenComponent: CombinedComponent { } state.updated(transition: .easeInOut(duration: 0.2)) } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag), - availableSize: CGSize(width: 30.0, height: 30.0), - transition: .immediate - ) - context.add(fillButton - .position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: topInset)) - .appear(.default(scale: true)) - .disappear(.default(scale: true)) - .opacity(!controlsAreVisible ? 0.0 : 1.0) - .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) - ) - } - - if hasFlip { - let flipButton = flipButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(.flip)) - ), + )) + } + + if hasFlip { + centerItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("flip"), + content: .customIcon(id: AnyHashable("flip"), component: AnyComponent( + Image(image: state.image(.flip), size: CGSize(width: 30.0, height: 30.0)) + ), insets: .zero), action: { [weak state] in guard let state = state else { return @@ -1695,17 +1682,8 @@ private final class DrawingScreenComponent: CombinedComponent { } state.updated(transition: .easeInOut(duration: 0.2)) } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(flipButtonTag), - availableSize: CGSize(width: 30.0, height: 30.0), - transition: .immediate - ) - context.add(flipButton - .position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: topInset)) - .appear(.default(scale: true)) - .disappear(.default(scale: true)) - .opacity(!controlsAreVisible ? 0.0 : 1.0) - .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) - ) + )) + } } } @@ -1730,28 +1708,24 @@ private final class DrawingScreenComponent: CombinedComponent { sizeValue = entity.lineWidth } } - if state.drawingViewState.canZoomOut && !hasTopButtons { - let zoomOutButton = zoomOutButton.update( - component: Button( - content: AnyComponent( - ZoomOutButtonContent( - title: strings.Paint_ZoomOut, - image: state.image(.zoomOut) - ) - ), - action: { - dismissEyedropper.invoke(Void()) - performAction.invoke(.zoomOut) - } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag), - availableSize: CGSize(width: 120.0, height: 33.0), - transition: .immediate - ) - context.add(zoomOutButton - .position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 32.0 - UIScreenPixel)) - .appear(.default(scale: true, alpha: true)) - .disappear(.default(scale: true, alpha: true)) - ) + if state.drawingViewState.canZoomOut && !hasTopButtons && controlsAreVisible { + centerItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("zoomOut"), + content: .customIcon(id: AnyHashable("zoomOut"), component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + Image(image: state.image(.zoomOut), size: CGSize(width: 24.0, height: 24.0)) + )), + AnyComponentWithIdentity(id: "label", component: AnyComponent( + Text(text: environment.strings.Paint_ZoomOut, font: Font.regular(17.0), color: .white) + )), + ], spacing: 2.0) + ), insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)), + action: { + dismissEyedropper.invoke(Void()) + performAction.invoke(.zoomOut) + } + )) } } if let sizeValue { @@ -1784,111 +1758,170 @@ private final class DrawingScreenComponent: CombinedComponent { .position(CGPoint(x: textSize.size.width / 2.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0)) .opacity(sizeSliderVisible && controlsAreVisible ? 1.0 : 0.0) ) - - let undoButton = undoButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(.undo)) - ), - isEnabled: state.drawingViewState.canUndo, - action: { + + var leftItems: [GlassControlGroupComponent.Item] = [] + var rightItems: [GlassControlGroupComponent.Item] = [] + + if !isEditingText && controlsAreVisible { + leftItems.append(GlassControlGroupComponent.Item( + id: "undo", + content: .customIcon(id: "undo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canUndo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0))), insets: .zero), + action: state.drawingViewState.canUndo ? { dismissEyedropper.invoke(Void()) performAction.invoke(.undo) - } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag), - availableSize: CGSize(width: 24.0, height: 24.0), + } : nil + )) + + if state.drawingViewState.canRedo { + leftItems.append(GlassControlGroupComponent.Item( + id: "redo", + content: .customIcon(id: "redo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canRedo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0), flipHorizontally: true)), insets: .zero), + action: state.drawingViewState.canRedo ? { + dismissEyedropper.invoke(Void()) + performAction.invoke(.redo) + } : nil + )) + } + } + + let undoButton = undoButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(Image(image: state.image(.undo))) + ), + action: { _ in + dismissEyedropper.invoke(Void()) + performAction.invoke(.undo) + }, + tag: undoButtonTag + ), + availableSize: CGSize(width: 44.0, height: 44.0), transition: context.transition ) context.add(undoButton - .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset)) + .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 16.0, y: topInset)) .scale(isEditingText ? 0.01 : 1.0) - .opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0) - .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) + .opacity(0.0) + //.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0) ) - let redoButton = redoButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(.redo)) + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(Image(image: state.image(.undo), flipHorizontally: true)) ), - action: { + action: { _ in dismissEyedropper.invoke(Void()) performAction.invoke(.redo) - } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag), - availableSize: CGSize(width: 24.0, height: 24.0), + }, + tag: redoButtonTag + ), + availableSize: CGSize(width: 44.0, height: 44.0), transition: context.transition ) context.add(redoButton .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset)) .scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01) - .opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0) - .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) + .opacity(0.0) + //.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0) ) let clearAllButton = clearAllButton.update( - component: Button( - content: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)), - textShadowColor: component.sourceHint == .storyEditor ? UIColor(rgb: 0x000000, alpha: 0.35) : nil, - textShadowBlur: 2.0 - ) + component: GlassBarButtonComponent( + size: nil, + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "label", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)) + )) ), - isEnabled: state.drawingViewState.canClear, - action: { + action: { _ in dismissEyedropper.invoke(Void()) performAction.invoke(.clear) - } - ).tagged(clearAllButtonTag), + }, + tag: clearAllButtonTag + ), availableSize: CGSize(width: 180.0, height: 30.0), transition: context.transition ) context.add(clearAllButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: topInset)) .scale(isEditingText ? 0.01 : 1.0) - .opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0) + .opacity(0.0) + //.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0) ) - let textCancelButton = textCancelButton.update( - component: Button( - content: AnyComponent( - Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white) - ), + if !isEditingText && controlsAreVisible { + rightItems.append(GlassControlGroupComponent.Item( + id: "clearAll", + content: .text(strings.Paint_Clear), + action: state.drawingViewState.canClear ? { + dismissEyedropper.invoke(Void()) + performAction.invoke(.clear) + } : nil + )) + } + + if isEditingText { + leftItems.append(GlassControlGroupComponent.Item( + id: "cancel", + content: .text(strings.Common_Cancel), action: { [weak state] in if let entity = state?.selectedEntity as? DrawingTextEntity { endEditingTextEntityView.invoke((entity.uuid, true)) } } - ), - availableSize: CGSize(width: 100.0, height: 30.0), - transition: context.transition - ) - context.add(textCancelButton - .position(CGPoint(x: environment.safeInsets.left + textCancelButton.size.width / 2.0 + 13.0, y: topInset)) - .scale(isEditingText ? 1.0 : 0.01) - .opacity(isEditingText ? 1.0 : 0.0) - ) - - let textDoneButton = textDoneButton.update( - component: Button( - content: AnyComponent( - Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white) - ), + )) + + rightItems.append(GlassControlGroupComponent.Item( + id: "done", + content: .text(strings.Common_Done), action: { [weak state] in if let entity = state?.selectedEntity as? DrawingTextEntity { endEditingTextEntityView.invoke((entity.uuid, false)) } } + )) + } + + let topButtons = topButtons.update( + component: GlassControlPanelComponent( + theme: defaultDarkPresentationTheme, + leftItem: leftItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: leftItems, + background: .panel + ), + centralItem: centerItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: centerItems, + background: .panel + ), + rightItem: rightItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: rightItems, + background: .panel + ), + centerAlignmentIfPossible: true, + isDark: true, + tag: topButtonsTag ), - availableSize: CGSize(width: 100.0, height: 30.0), + availableSize: CGSize(width: context.availableSize.width - 32.0, height: 44.0), transition: context.transition ) - context.add(textDoneButton - .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: topInset)) - .scale(isEditingText ? 1.0 : 0.01) - .opacity(isEditingText ? 1.0 : 0.0) + context.add(topButtons + .position(CGPoint(x: context.availableSize.width / 2.0, y: topInset)) + //.opacity(isEditingText ? 1.0 : 0.0) ) var color: DrawingColor? @@ -1930,7 +1963,7 @@ private final class DrawingScreenComponent: CombinedComponent { transition: context.transition ) context.add(colorButton - .position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset)) + .position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 6.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset)) .appear(.default(scale: true)) .disappear(.default(scale: true)) .opacity(controlsAreVisible ? 1.0 : 0.0) @@ -1979,7 +2012,7 @@ private final class DrawingScreenComponent: CombinedComponent { transition: .immediate ) context.add(addButton - .position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 2.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset)) + .position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 6.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset)) .appear(.default(scale: true)) .disappear(.default(scale: true)) .cornerRadius(12.0) @@ -1987,27 +2020,38 @@ private final class DrawingScreenComponent: CombinedComponent { ) let doneButton = doneButton.update( - component: Button( - content: AnyComponent( - Image(image: state.image(.done)) + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: UIColor(rgb: 0x0088ff), + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Done", tintColor: .white) + ) ), - action: { [weak state] in + action: { [weak state] _ in dismissEyedropper.invoke(Void()) state?.saveToolState() apply.invoke(Void()) - } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag), - availableSize: CGSize(width: 33.0, height: 33.0), + }, + tag: doneButtonTag + ), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) - var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel) + var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 14.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel) if component.sourceHint == .storyEditor { doneButtonPosition.x = doneButtonPosition.x - 2.0 if case .regular = environment.metrics.widthClass { doneButtonPosition.x -= 20.0 } - doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset + doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset + 5.0 + } else { + doneButtonPosition.x = doneButtonPosition.x - 12.0 + doneButtonPosition.y -= 3.0 - UIScreenPixel } context.add(doneButton .position(doneButtonPosition) @@ -2026,117 +2070,82 @@ private final class DrawingScreenComponent: CombinedComponent { }) .opacity(controlsAreVisible ? 1.0 : 0.0) ) - - let selectedIndex: Int - switch state.currentMode { - case .drawing: - selectedIndex = 0 - case .sticker: - selectedIndex = 1 - case .text: - selectedIndex = 2 - } - - var selectedSize: CGFloat = 0.0 - if let entity = state.selectedEntity { - selectedSize = entity.lineWidth - } else { - selectedSize = state.drawingState.toolState(for: state.drawingState.selectedTool).size ?? 0.0 - } - - let modeAndSize = modeAndSize.update( - component: ModeAndSizeComponent( - values: [ strings.Paint_Draw, strings.Paint_Sticker, strings.Paint_Text], - sizeValue: selectedSize, - isEditing: false, - isEnabled: true, - rightInset: modeRightInset - 57.0, - tag: modeTag, - selectedIndex: selectedIndex, - selectionChanged: { [weak state] index in - dismissEyedropper.invoke(Void()) - guard let state = state else { - return - } - switch index { - case 1: - state.presentStickerPicker() - case 2: - state.addTextEntity() - default: - state.updateCurrentMode(.drawing) - } - }, - sizeUpdated: { [weak state] size in - if let state = state { + + let mode = mode.update( + component: ModeComponent( + isTablet: false, + strings: environment.strings, + tintColor: .white, + availableModes: [.drawing, .sticker, .text], + currentMode: state.currentMode, + updatedMode: { [weak state] mode in + if let state { dismissEyedropper.invoke(Void()) - state.updateBrushSize(size) - if state.selectedEntity == nil { - previewBrushSize.invoke(size) + switch mode { + case .drawing: + state.updateCurrentMode(.drawing) + case .sticker: + state.presentStickerPicker() + case .text: + state.addTextEntity() } } }, - sizeReleased: { - previewBrushSize.invoke(nil) - } + tag: modeTag ), - availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height), + availableSize: CGSize(width: context.availableSize.width - 66.0 * 2.0, height: 44.0), transition: context.transition ) - var modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0) + var modePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - mode.size.height / 2.0 - 9.0) if component.sourceHint == .storyEditor { - modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + modeAndSize.size.height / 2.0) + controlsBottomInset + modePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + mode.size.height / 2.0) + controlsBottomInset + } else { + modePosition.y += 4.0 } - context.add(modeAndSize - .position(modeAndSizePosition) + context.add(mode + .position(modePosition) .opacity(controlsAreVisible ? 1.0 : 0.0) ) - var animatingOut = false - if let appearanceTransition = context.transition.userData(DrawingScreenTransition.self), case .animateOut = appearanceTransition { - animatingOut = true - } - - if animatingOut && component.sourceHint == .storyEditor { - - } else { - let backButton = backButton.update( - component: Button( - content: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "media_backToCancel", - mode: .animating(loop: false), - range: animatingOut || component.isAvatar ? (0.5, 1.0) : (0.0, 0.5) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 33.0, height: 33.0) - ) - ), - action: { [weak state] in - if let state = state { - dismissEyedropper.invoke(Void()) - state.saveToolState() - dismiss.invoke(Void()) - } + let backButton = backButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Close", tintColor: .white) + ) + ), + action: { [weak state] _ in + if let state { + dismissEyedropper.invoke(Void()) + state.saveToolState() + dismiss.invoke(Void()) } - ).minSize(CGSize(width: 44.0, height: 44.0)).tagged(cancelButtonTag), - availableSize: CGSize(width: 33.0, height: 33.0), - transition: .immediate - ) - var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel) - if component.sourceHint == .storyEditor { - backButtonPosition.x = backButtonPosition.x + 2.0 - if case .regular = environment.metrics.widthClass { - backButtonPosition.x += 20.0 - } - backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset + }, + tag: cancelButtonTag + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: .immediate + ) + var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 14.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel) + if component.sourceHint == .storyEditor { + backButtonPosition.x = backButtonPosition.x + 2.0 + if case .regular = environment.metrics.widthClass { + backButtonPosition.x += 20.0 } - context.add(backButton - .position(backButtonPosition) - .opacity(controlsAreVisible ? 1.0 : 0.0) - ) + backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset + 5.0 + } else { + backButtonPosition.x = backButtonPosition.x + 12.0 + backButtonPosition.y -= 3.0 - UIScreenPixel } + context.add(backButton + .position(backButtonPosition) + .opacity(controlsAreVisible ? 1.0 : 0.0) + ) return context.availableSize } @@ -2224,22 +2233,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.performAction.connect { [weak self] action in if let self { if case .clear = action { - let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() - - self?._drawingView?.performAction(action) - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - self.controller?.present(actionSheet, in: .window(.root)) + let sourceView: UIView + if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View, let rightItemView = topButtonsView.rightItemView, let clearAllView = rightItemView.itemView(id: AnyHashable("clearAll")) { + sourceView = clearAllView + } else { + sourceView = self.view + } + + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: self.presentationData.strings.Paint_ClearConfirm, textColor: .destructive, icon: { _ in + return nil + }, action: { [weak self] f in + f.dismissWithResult(.default) + self?._drawingView?.performAction(.clear) + })) + ] + let presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(), actionsPosition: .bottom)), items: .single(ContextController.Items(content: .list(items)))) + self.controller?.present(contextController, in: .window(.root)) } else { self._drawingView?.performAction(action) } @@ -2403,22 +2414,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.dismiss.connect { [weak self] _ in if let strongSelf = self { if strongSelf.drawingView.canUndo || strongSelf.entitiesView.hasChanges { - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)) - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, color: .accent, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() - - self?.controller?.requestDismiss() - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - strongSelf.controller?.present(actionSheet, in: .window(.root)) + let sourceView = strongSelf.componentHost.findTaggedView(tag: cancelButtonTag) ?? strongSelf.view + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, textColor: .destructive, icon: { _ in + return nil + }, action: { [weak self] f in + f.dismissWithResult(.default) + self?.controller?.requestDismiss() + })) + ] + let presentationData = strongSelf.presentationData.withUpdated(theme: defaultDarkPresentationTheme) + let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint())), items: .single(ContextController.Items(content: .list(items)))) + strongSelf.controller?.present(contextController, in: .window(.root)) } else { strongSelf.controller?.requestDismiss() } @@ -2497,17 +2504,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U if let view = self.componentHost.findTaggedView(tag: bottomGradientTag) { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } - if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) { - buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View { + topButtonsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + if let leftItemView = topButtonsView.leftItemView { + leftItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + leftItemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + if let rightItemView = topButtonsView.rightItemView { + rightItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + rightItemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } } - if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) { - buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + if let view = self.componentHost.findTaggedView(tag: modeTag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } - if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) { - buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + if let view = self.componentHost.findTaggedView(tag: addButtonTag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) } var delay: Double = 0.0 for tag in colorTags { @@ -2520,6 +2534,15 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) { view.layer.animatePosition(from: CGPoint(x: -33.0, y: 0.0), to: CGPoint(), duration: 0.3, additive: true) } + + if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } + if let view = self.componentHost.findTaggedView(tag: doneButtonTag) { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3) + } } func animateOut(completion: @escaping () -> Void) { @@ -2536,43 +2559,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U } view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) } - if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) { - buttonView.alpha = 0.0 - buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) - } - if let buttonView = self.componentHost.findTaggedView(tag: redoButtonTag), buttonView.alpha > 0.0 { - buttonView.alpha = 0.0 - buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) - } - if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) { - buttonView.alpha = 0.0 - buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View { + topButtonsView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + + if let view = topButtonsView.leftItemView { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false) + } + if let view = topButtonsView.rightItemView { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false) + } } if let view = self.componentHost.findTaggedView(tag: colorButtonTag) as? ColorSwatchComponent.View { view.animateOut() } - if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) { - buttonView.alpha = 0.0 - buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) - } - if let buttonView = self.componentHost.findTaggedView(tag: flipButtonTag) { - buttonView.alpha = 0.0 - buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) - } - if let buttonView = self.componentHost.findTaggedView(tag: fillButtonTag) { - buttonView.alpha = 0.0 - buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) - } - if let buttonView = self.componentHost.findTaggedView(tag: zoomOutButtonTag) { - buttonView.alpha = 0.0 - buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) + if let view = self.componentHost.findTaggedView(tag: addButtonTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false) } if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) { view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true) @@ -2596,12 +2600,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U }) } - if let view = self.componentHost.findTaggedView(tag: modeTag) as? ModeAndSizeComponent.View { - view.animateOut() + if let view = self.componentHost.findTaggedView(tag: modeTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) } - if let buttonView = self.componentHost.findTaggedView(tag: doneButtonTag) { - buttonView.alpha = 0.0 - buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) + + if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false) + } + + if let view = self.componentHost.findTaggedView(tag: doneButtonTag) { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3) } } 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/DrawingUI/Sources/ModeAndSizeComponent.swift b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift index a8822e5230..0872e00b73 100644 --- a/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift +++ b/submodules/DrawingUI/Sources/ModeAndSizeComponent.swift @@ -2,95 +2,123 @@ import Foundation import UIKit import Display import ComponentFlow -import LegacyComponents -import TelegramCore -import SegmentedControlNode +import MultilineTextComponent +import TelegramPresentationData +import GlassBackgroundComponent +import LiquidLens +import TabSelectionRecognizer -private func generateMaskPath(size: CGSize, leftRadius: CGFloat, rightRadius: CGFloat) -> UIBezierPath { - let path = UIBezierPath() - path.addArc(withCenter: CGPoint(x: leftRadius, y: size.height / 2.0), radius: leftRadius, startAngle: .pi * 0.5, endAngle: -.pi * 0.5, clockwise: true) - path.addArc(withCenter: CGPoint(x: size.width - rightRadius, y: size.height / 2.0), radius: rightRadius, startAngle: -.pi * 0.5, endAngle: .pi * 0.5, clockwise: true) - path.close() - return path +private let buttonSize = CGSize(width: 55.0, height: 44.0) +private let tabletButtonSize = CGSize(width: 55.0, height: 44.0) + +extension DrawingMode { + func title(strings: PresentationStrings) -> String { + switch self { + case .drawing: + return strings.Paint_Draw + case .sticker: + return strings.Paint_Sticker + case .text: + return strings.Paint_Text + } + } } -private func generateKnobImage() -> UIImage? { - let side: CGFloat = 28.0 - let margin: CGFloat = 10.0 - - let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in - context.clear(CGRect(origin: .zero, size: size)) - - context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side))) - }) - return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5)) -} - -final class ModeAndSizeComponent: Component { - let values: [String] - let sizeValue: CGFloat - let isEditing: Bool - let isEnabled: Bool - let rightInset: CGFloat +final class ModeComponent: Component { + let isTablet: Bool + let strings: PresentationStrings + let tintColor: UIColor + let availableModes: [DrawingMode] + let currentMode: DrawingMode + let updatedMode: (DrawingMode) -> Void let tag: AnyObject? - let selectedIndex: Int - let selectionChanged: (Int) -> Void - let sizeUpdated: (CGFloat) -> Void - let sizeReleased: () -> Void - init(values: [String], sizeValue: CGFloat, isEditing: Bool, isEnabled: Bool, rightInset: CGFloat, tag: AnyObject?, selectedIndex: Int, selectionChanged: @escaping (Int) -> Void, sizeUpdated: @escaping (CGFloat) -> Void, sizeReleased: @escaping () -> Void) { - self.values = values - self.sizeValue = sizeValue - self.isEditing = isEditing - self.isEnabled = isEnabled - self.rightInset = rightInset + init( + isTablet: Bool, + strings: PresentationStrings, + tintColor: UIColor, + availableModes: [DrawingMode], + currentMode: DrawingMode, + updatedMode: @escaping (DrawingMode) -> Void, + tag: AnyObject? + ) { + self.isTablet = isTablet + self.strings = strings + self.tintColor = tintColor + self.availableModes = availableModes + self.currentMode = currentMode + self.updatedMode = updatedMode self.tag = tag - self.selectedIndex = selectedIndex - self.selectionChanged = selectionChanged - self.sizeUpdated = sizeUpdated - self.sizeReleased = sizeReleased } - static func ==(lhs: ModeAndSizeComponent, rhs: ModeAndSizeComponent) -> Bool { - if lhs.values != rhs.values { + static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool { + if lhs.isTablet != rhs.isTablet { return false } - if lhs.sizeValue != rhs.sizeValue { + if lhs.strings !== rhs.strings { return false } - if lhs.isEditing != rhs.isEditing { + if lhs.tintColor != rhs.tintColor { return false } - if lhs.isEnabled != rhs.isEnabled { + if lhs.availableModes != rhs.availableModes { return false } - if lhs.rightInset != rhs.rightInset { - return false - } - if lhs.selectedIndex != rhs.selectedIndex { + if lhs.currentMode != rhs.currentMode { return false } return true } - - final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView { - private let backgroundNode: NavigationBackgroundNode - private let node: SegmentedControlNode + + final class View: UIView, ComponentTaggedView, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } - private var knob: UIImageView + private struct LayoutData { + var containerSize: CGSize + var selectedFrame: CGRect + var cornerRadius: CGFloat? + var isTablet: Bool + } - private let maskLayer = SimpleShapeLayer() + private var component: ModeComponent? + private var state: EmptyComponentState? - private var isEditing: Bool? - private var isControlEnabled: Bool? - private var sliderWidth: CGFloat = 0.0 + final class ItemView: HighlightTrackingButton { + init() { + super.init(frame: .zero) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize { + let title = NSMutableAttributedString(string: value, font: Font.with(size: 15.0, design: .regular, weight: .medium), textColor: UIColor(rgb: 0xffffff), paragraphAlignment: .center) + self.setAttributedTitle(title, for: .normal) + self.sizeToFit() + return CGSize(width: self.titleLabel?.bounds.size.width ?? 0.0, height: buttonSize.height) + } + } - fileprivate var updated: (CGFloat) -> Void = { _ in } - fileprivate var released: () -> Void = { } + private var backgroundView = UIView() + private var backgroundContainer = GlassBackgroundContainerView() + + private var liquidLensView: LiquidLensView? + private let scrollView = ScrollView() + private let selectedScrollView = UIView() + private var ignoreScrolling = false + private var layoutData: LayoutData? + + private var itemViews: [AnyHashable: ItemView] = [:] + private var selectedItemViews: [AnyHashable: ItemView] = [:] + + private var tabSelectionRecognizer: TabSelectionRecognizer? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? - private var component: ModeAndSizeComponent? public func matches(tag: Any) -> Bool { if let component = self.component, let componentTag = component.tag { let tag = tag as AnyObject @@ -102,164 +130,350 @@ final class ModeAndSizeComponent: Component { } init() { - self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3)) - self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0, cornerRadius: 16.0) - - self.knob = UIImageView(image: generateKnobImage()) - super.init(frame: CGRect()) + self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.09) + self.backgroundView.layer.cornerRadius = 22.0 + self.layer.allowsGroupOpacity = true - - self.addSubview(self.backgroundNode.view) - self.addSubview(self.node.view) - self.addSubview(self.knob) - self.backgroundNode.layer.mask = self.maskLayer + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = false + self.scrollView.scrollsToTop = false + self.scrollView.clipsToBounds = true + self.scrollView.delegate = self + self.scrollView.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let self else { + return false + } + return self.scrollView.contentOffset.x > .ulpOfOne + } - let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:))) - pressGestureRecognizer.minimumPressDuration = 0.01 - pressGestureRecognizer.delegate = self - self.addGestureRecognizer(pressGestureRecognizer) + self.selectedScrollView.clipsToBounds = true + self.selectedScrollView.isUserInteractionEnabled = false - let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) - panGestureRecognizer.delegate = self - self.addGestureRecognizer(panGestureRecognizer) + self.addSubview(self.backgroundView) + self.backgroundView.addSubview(self.backgroundContainer) } required init?(coder aDecoder: NSCoder) { preconditionFailure() } - @objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) { - let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0) - guard self.frame.width > 0.0, case .began = gestureRecognizer.state else { - return - } - let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0))) - self.updated(value) + private var animatedOut = false + func animateOutToEditor(transition: ComponentTransition) { + self.animatedOut = true + + transition.setAlpha(view: self.backgroundView, alpha: 0.0) + transition.setSublayerTransform(view: self, transform: CATransform3DMakeTranslation(0.0, -buttonSize.height, 0.0)) } - @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { - switch gestureRecognizer.state { - case .changed: - let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0) - guard self.frame.width > 0.0 else { - return + func animateInFromEditor(transition: ComponentTransition) { + self.animatedOut = false + + transition.setAlpha(view: self.backgroundView, alpha: 1.0) + transition.setSublayerTransform(view: self, transform: CATransform3DIdentity) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.backgroundView.frame.contains(point) + } + + private func item(at point: CGPoint, in view: UIView) -> AnyHashable? { + var closestItem: (AnyHashable, CGFloat)? + for (id, itemView) in self.itemViews { + let itemFrame = itemView.convert(itemView.bounds, to: view) + if itemFrame.contains(point) { + return id + } else { + let distance = abs(point.x - itemFrame.midX) + if let closestItemValue = closestItem { + if closestItemValue.1 > distance { + closestItem = (id, distance) + } + } else { + closestItem = (id, distance) + } + } + } + return closestItem?.0 + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + self.updateScrolling(transition: .immediate) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === self.tabSelectionRecognizer && otherGestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } + if otherGestureRecognizer === self.tabSelectionRecognizer && gestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } + return false + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let component = self.component else { + return + } + let location = recognizer.location(in: self) + switch recognizer.state { + case .began: + if let itemId = self.item(at: location, in: self), let itemView = self.itemViews[itemId] { + let startX = itemView.frame.minX - 4.0 + self.selectionGestureState = (startX, startX, itemId) + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + case .changed: + if var selectionGestureState = self.selectionGestureState { + let translation = recognizer.translation(in: self) + if !component.isTablet && self.scrollView.isScrollEnabled && abs(translation.x) > 6.0 && abs(translation.x) > abs(translation.y) { + self.selectionGestureState = nil + recognizer.state = .cancelled + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + return + } + selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x + if let itemId = self.item(at: location, in: self) { + selectionGestureState.itemId = itemId + } + self.selectionGestureState = selectionGestureState + self.state?.updated(transition: .immediate, isLocal: true) } - let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0))) - self.updated(value) case .ended, .cancelled: - self.released() + if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state { + guard let item = component.availableModes.first(where: { AnyHashable($0.rawValue) == selectionGestureState.itemId }) else { + return + } + component.updatedMode(item) + } + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } default: break } } - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if let isEditing = self.isEditing, let isControlEnabled = self.isControlEnabled { - return isEditing && isControlEnabled + private func updateScrolling(transition: ComponentTransition) { + guard let component = self.component, let liquidLensView = self.liquidLensView, let layoutData = self.layoutData else { + return + } + + let contentOffsetX = layoutData.isTablet ? 0.0 : self.scrollView.bounds.minX + var lensSelection = (origin: layoutData.selectedFrame.origin, size: layoutData.selectedFrame.size) + if let selectionGestureState = self.selectionGestureState, !layoutData.isTablet { + lensSelection.origin = CGPoint(x: selectionGestureState.currentX, y: 0.0) + } + + if layoutData.isTablet { + lensSelection.size.width = layoutData.containerSize.width } else { - return false + lensSelection.origin.x -= contentOffsetX + lensSelection.origin.y = 0.0 + lensSelection.size.height = layoutData.containerSize.height } + + let maxSelectionOriginX = max(0.0, layoutData.containerSize.width - lensSelection.size.width) + transition.setFrame(view: self.selectedScrollView, frame: CGRect(origin: .zero, size: layoutData.containerSize)) + transition.setBounds(view: self.selectedScrollView, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: layoutData.containerSize)) + + liquidLensView.update(size: layoutData.containerSize, cornerRadius: layoutData.cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, maxSelectionOriginX)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !layoutData.isTablet, isCollapsed: false, transition: transition) + self.backgroundContainer.update(size: layoutData.containerSize, isDark: true, transition: .immediate) + + self.scrollView.isScrollEnabled = !component.isTablet && self.scrollView.contentSize.width > self.scrollView.bounds.width + .ulpOfOne } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } - - func animateIn() { - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - } - - func animateOut() { - self.node.alpha = 0.0 - self.node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - - self.backgroundNode.alpha = 0.0 - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - - func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { - self.component = component - - self.updated = component.sizeUpdated - self.released = component.sizeReleased - - let previousIsEditing = self.isEditing - self.isEditing = component.isEditing - self.isControlEnabled = component.isEnabled - - if component.isEditing { - self.sliderWidth = availableSize.width - } - - self.node.items = component.values.map { SegmentedControlItem(title: $0) } - self.node.setSelectedIndex(component.selectedIndex, animated: !transition.animation.isImmediate) - let selectionChanged = component.selectionChanged - self.node.selectedIndexChanged = { [weak self] index in - self?.window?.endEditing(true) - selectionChanged(index) - } - - let nodeSize = self.node.updateLayout(.stretchToFill(width: availableSize.width + component.rightInset), transition: transition.containedViewLayoutTransition) - let size = CGSize(width: availableSize.width, height: nodeSize.height) - transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: nodeSize)) - - var isDismissingEditing = false - if component.isEditing != previousIsEditing && !component.isEditing { - isDismissingEditing = true - } - - self.knob.alpha = component.isEditing ? 1.0 : 0.0 - if !isDismissingEditing { - self.knob.frame = CGRect(origin: CGPoint(x: -12.0 + floorToScreenPixels((self.sliderWidth + 24.0 - self.knob.frame.size.width) * component.sizeValue), y: floorToScreenPixels((size.height - self.knob.frame.size.height) / 2.0)), size: self.knob.frame.size) - } - if component.isEditing != previousIsEditing { - let containedTransition = transition.containedViewLayoutTransition - let maskPath: UIBezierPath - if component.isEditing { - maskPath = generateMaskPath(size: size, leftRadius: 2.0, rightRadius: 11.5) - let selectionFrame = self.node.animateSelection(to: self.knob.center, transition: containedTransition) - containedTransition.animateFrame(layer: self.knob.layer, from: selectionFrame.insetBy(dx: -9.0, dy: -9.0)) - - self.knob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state + + let isTablet = component.isTablet + + let liquidLensView: LiquidLensView + if let current = self.liquidLensView { + liquidLensView = current + } else { + liquidLensView = LiquidLensView(kind: isTablet ? .noContainer : .externalContainer) + self.liquidLensView = liquidLensView + self.backgroundContainer.contentView.addSubview(liquidLensView) + liquidLensView.contentView.addSubview(self.scrollView) + liquidLensView.selectedContentView.addSubview(self.selectedScrollView) + + let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + tabSelectionRecognizer.delegate = self + tabSelectionRecognizer.cancelsTouchesInView = false + self.tabSelectionRecognizer = tabSelectionRecognizer + liquidLensView.addGestureRecognizer(tabSelectionRecognizer) + } + if self.scrollView.superview == nil { + liquidLensView.contentView.addSubview(self.scrollView) + } + if self.selectedScrollView.superview == nil { + liquidLensView.selectedContentView.addSubview(self.selectedScrollView) + } + + self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11) + + var inset: CGFloat = 23.0 + let spacing: CGFloat + if isTablet { + spacing = 9.0 + } else { + if availableSize.width < 200.0 { + inset = 20.0 + spacing = 24.0 } else { - maskPath = generateMaskPath(size: size, leftRadius: 16.0, rightRadius: 16.0) - if previousIsEditing != nil { - let selectionFrame = self.node.animateSelection(from: self.knob.center, transition: containedTransition) - containedTransition.animateFrame(layer: self.knob.layer, from: self.knob.frame, to: selectionFrame.insetBy(dx: -9.0, dy: -9.0)) - self.knob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + spacing = 30.0 + } + } + + var i = 0 + var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize) + var selectedFrame = itemFrame + + var validKeys: Set = Set() + for mode in component.availableModes { + let id = mode.rawValue + validKeys.insert(id) + + let itemView: ItemView + let selectedItemView: ItemView + if let current = self.itemViews[id], let currentSelected = self.selectedItemViews[id] { + itemView = current + selectedItemView = currentSelected + } else { + itemView = ItemView() + itemView.isUserInteractionEnabled = false + self.itemViews[id] = itemView + + selectedItemView = ItemView() + selectedItemView.isUserInteractionEnabled = false + self.selectedItemViews[id] = selectedItemView + } + if itemView.superview !== self.scrollView { + self.scrollView.addSubview(itemView) + } + if selectedItemView.superview !== self.selectedScrollView { + self.selectedScrollView.addSubview(selectedItemView) + } + + let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor) + itemView.bounds = CGRect(origin: .zero, size: itemSize) + + let _ = selectedItemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: true, tintColor: component.tintColor) + selectedItemView.bounds = CGRect(origin: .zero, size: itemSize) + + itemFrame = CGRect(origin: itemFrame.origin, size: itemSize) + + if mode == component.currentMode { + selectedFrame = itemFrame + } + + if isTablet { + itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY) + selectedItemView.center = itemView.center + itemFrame = itemFrame.offsetBy(dx: 0.0, dy: tabletButtonSize.height + spacing) + } else { + itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + selectedItemView.center = itemView.center + itemFrame = itemFrame.offsetBy(dx: itemFrame.width + spacing, dy: 0.0) + } + i += 1 + } + + var removeKeys: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validKeys.contains(id) { + removeKeys.append(id) + + transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + itemView.removeFromSuperview() + }) + + if let selectedItemView = self.selectedItemViews[id] { + transition.setAlpha(view: selectedItemView, alpha: 0.0, completion: { _ in + selectedItemView.removeFromSuperview() + }) } } - transition.setShapeLayerPath(layer: self.maskLayer, path: maskPath.cgPath) } - - transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: nodeSize)) + for id in removeKeys { + self.itemViews.removeValue(forKey: id) + self.selectedItemViews.removeValue(forKey: id) + } - transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition) + let totalSize: CGSize + let size: CGSize + let contentSize: CGSize + var cornerRadius: CGFloat? + if isTablet { + totalSize = CGSize(width: availableSize.width, height: tabletButtonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1)) + size = CGSize(width: availableSize.width, height: availableSize.height) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: totalSize)) + contentSize = totalSize + cornerRadius = 20.0 + } else { + size = CGSize(width: availableSize.width, height: buttonSize.height) + totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height) + let visibleSize = CGSize(width: min(availableSize.width, totalSize.width), height: totalSize.height) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - visibleSize.width) / 2.0), y: 0.0), size: visibleSize)) + contentSize = totalSize + } - if let screenTransition = transition.userData(DrawingScreenTransition.self) { - switch screenTransition { - case .animateIn: - self.animateIn() - case .animateOut: - self.animateOut() + let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size) + transition.setFrame(view: self.backgroundContainer, frame: containerFrame) + transition.setFrame(view: liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size)) + + let scrollViewFrame = CGRect(origin: .zero, size: containerFrame.size) + transition.setFrame(view: self.scrollView, frame: scrollViewFrame) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + self.scrollView.isScrollEnabled = !isTablet && contentSize.width > scrollViewFrame.width + .ulpOfOne + + self.layoutData = LayoutData(containerSize: containerFrame.size, selectedFrame: selectedFrame.insetBy(dx: -inset, dy: 3.0), cornerRadius: cornerRadius, isTablet: isTablet) + + self.ignoreScrolling = true + var scrollViewBounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollViewFrame.size) + let maxContentOffsetX = max(0.0, contentSize.width - scrollViewFrame.width) + let shouldFocusOnSelectedItem = previousComponent?.currentMode != component.currentMode || previousComponent?.availableModes != component.availableModes || self.scrollView.bounds.size != scrollViewFrame.size + if self.scrollView.isScrollEnabled && shouldFocusOnSelectedItem { + let scrollLookahead = min(60.0, scrollViewBounds.width * 0.25) + if scrollViewBounds.minX + scrollViewBounds.width - scrollLookahead < selectedFrame.maxX { + scrollViewBounds.origin.x = selectedFrame.maxX - scrollViewBounds.width + scrollLookahead + } + if scrollViewBounds.minX > selectedFrame.minX - scrollLookahead { + scrollViewBounds.origin.x = selectedFrame.minX - scrollLookahead } } + scrollViewBounds.origin.x = max(0.0, min(scrollViewBounds.origin.x, maxContentOffsetX)) + transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) 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) + return view.update(component: self, availableSize: availableSize, state: state, transition: transition) } } diff --git a/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift b/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift index 580693d0f1..e9d3014d93 100644 --- a/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift +++ b/submodules/FeaturedStickersScreen/Sources/ChatMediaInputTrendingPane.swift @@ -254,6 +254,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 { @@ -373,7 +437,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/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 88e6dc1925..ccf2c4a8dd 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -675,6 +675,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll super.didLoad() self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false let backwardLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.seekBackwardLongPress(_:))) backwardLongPressGestureRecognizer.minimumPressDuration = 0.3 @@ -1356,7 +1357,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll speed: settingsButtonState.speed, quality: settingsButtonState.quality, isOpen: false - ))), + )), insets: .zero), action: { [weak self] in guard let self, let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View else { return @@ -1819,12 +1820,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if availableOpenInOptions(context: strongSelf.context, item: item).count > 1 { preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in if let strongSelf = self { - let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in + let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in if let strongSelf = self { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } }) - strongSelf.controllerInteraction?.presentController(openInController, nil) + strongSelf.controllerInteraction?.pushController(openInController) } })) } else { @@ -2124,12 +2125,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll if availableOpenInOptions(context: self.context, item: item).count > 1 { preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in if let strongSelf = self { - let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in + let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in if let strongSelf = self { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } }) - strongSelf.controllerInteraction?.presentController(openInController, nil) + strongSelf.controllerInteraction?.pushController(openInController) } })) } else { diff --git a/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift b/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift index 1f7e9247e8..1c5ef1273b 100644 --- a/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryThumbnailContainerNode.swift @@ -83,6 +83,7 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, ASScrollViewDel self.scrollNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 5919706cff..a70cc1f360 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -419,6 +419,9 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in adView.removeFromSuperview() + if self.adView.view === adView { + self.adView = ComponentView() + } Queue.mainQueue().after(0.1) { adView.layer.removeAllAnimations() } @@ -444,7 +447,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN return result } } - if let adView = self.adView.view, adView.frame.contains(point) { + if let adView = self.adView.view, adView.superview === self.view, !self.isAnimatingOut, adView.frame.contains(point) { return super.hitTest(point, with: event) } return nil @@ -3892,12 +3895,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if !presentationData.theme.overallDarkAppearance { presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) } - let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in if let strongSelf = self { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {}) } }) - controller.present(actionSheet, in: .window(.root)) + controller.push(actionSheet) } }))) break diff --git a/submodules/GlassButtonNode/BUILD b/submodules/GlassButtonNode/BUILD deleted file mode 100644 index bab4d6718e..0000000000 --- a/submodules/GlassButtonNode/BUILD +++ /dev/null @@ -1,19 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "GlassButtonNode", - module_name = "GlassButtonNode", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/Display:Display", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/GlassButtonNode/Info.plist b/submodules/GlassButtonNode/Info.plist deleted file mode 100644 index e1fe4cfb7b..0000000000 --- a/submodules/GlassButtonNode/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - - diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift index 4e395aaf5a..b2d7db0d72 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift @@ -307,7 +307,7 @@ final class HashtagSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { if item.clear { strongSelf.setRevealOptions((left: [], right: [])) } else { - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.theme.list.itemSecondaryTextColor)])) } } }) diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index a00b0f3073..31ea269639 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -110,6 +110,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 2c3bd0525c..69f1f2b0ba 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -139,6 +139,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { self.addSubnode(self.navigationBar) self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.navigationBar.back = navigateBack self.navigationBar.share = { [weak self] in @@ -1787,12 +1788,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { private func openUrlIn(_ url: InstantPageUrlItem) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } }) - self.present(actionSheet, nil) + self.pushController(actionSheet) } private func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] { diff --git a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift index dee69b4aac..8fe23019a6 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift @@ -309,7 +309,21 @@ public final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode @objc func joinPressed() { if let peer = self.peer, case .notJoined = self.joinState { self.updateJoinState(.inProgress) - self.joinDisposable.set((self.context.engine.peers.joinChannel(peerId: peer.id, hash: nil) |> deliverOnMainQueue).start(error: { [weak self] _ in + self.joinDisposable.set((self.context.engine.peers.joinChannel(peerId: peer.id, hash: nil) |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + switch result { + case .joined: + break + case let .webView(webView): + if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController, let controller = navigationController.viewControllers.last as? ViewController { + strongSelf.context.sharedContext.openJoinChatWebView(context: strongSelf.context, parentController: controller, updatedPresentationData: nil, webView: webView) + } else if case .inProgress = strongSelf.joinState { + strongSelf.updateJoinState(.notJoined) + } + } + }, error: { [weak self] _ in if let strongSelf = self { if case .inProgress = strongSelf.joinState { strongSelf.updateJoinState(.notJoined) diff --git a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift index 50c1222805..c60553a9c8 100644 --- a/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageReferenceControllerNode.swift @@ -52,6 +52,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/InviteLinksUI/BUILD b/submodules/InviteLinksUI/BUILD index bed130117c..f5c6a4fb6b 100644 --- a/submodules/InviteLinksUI/BUILD +++ b/submodules/InviteLinksUI/BUILD @@ -60,6 +60,7 @@ swift_library( "//submodules/PromptUI", "//submodules/TelegramUI/Components/ItemListDatePickerItem:ItemListDatePickerItem", "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/ComponentFlow", "//submodules/Components/MultilineTextComponent", ], diff --git a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift index bde2b78d3a..d37959cda9 100644 --- a/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift +++ b/submodules/InviteLinksUI/Sources/FolderInviteLinkListController.swift @@ -711,7 +711,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese saveEnabled = true } - doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Save), style: .bold, enabled: !state.selectedPeerIds.isEmpty && saveEnabled, action: { + doneButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: !state.selectedPeerIds.isEmpty && saveEnabled, action: { applyChangesImpl?() }) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 8cbb4e2389..e0c4440b86 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -479,7 +479,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { } } -private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, presentationData: PresentationData, configuration: StarsSubscriptionConfiguration) -> [InviteLinksEditEntry] { +private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: InviteLinkEditControllerState, isGroup: Bool, isPublic: Bool, globalRequestApproval: Bool, presentationData: PresentationData, configuration: StarsSubscriptionConfiguration) -> [InviteLinksEditEntry] { var entries: [InviteLinksEditEntry] = [] entries.append(.titleHeader(presentationData.theme, presentationData.strings.InviteLink_Create_LinkNameTitle.uppercased())) @@ -507,17 +507,14 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: entries.append(.subscriptionFeeInfo(presentationData.theme, infoText)) } - if !isPublic { - entries.append(.requestApproval(presentationData.theme, presentationData.strings.InviteLink_Create_RequestApproval, state.requestApproval, isEditingEnabled && !isSubscription)) - var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel + if !isPublic || isGroup { + let requestApprovalEnabled = isEditingEnabled && !isSubscription && !(isPublic && isGroup && !globalRequestApproval) + entries.append(.requestApproval(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_ApproveNewMembers : presentationData.strings.Channel_Setup_ApproveNewSubscribers, state.requestApproval, requestApprovalEnabled)) + var requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalInfo if isSubscription { requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalFeeUnavailable - } else { - if state.requestApproval { - requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOnInfoChannel - } else { - requestApprovalInfoText = isGroup ? presentationData.strings.InviteLink_Create_RequestApprovalOnInfoGroup : presentationData.strings.InviteLink_Create_RequestApprovalOffInfoChannel - } + } else if isPublic && isGroup && !globalRequestApproval { + requestApprovalInfoText = presentationData.strings.InviteLink_Create_RequestApprovalPublicGroupUnavailable } entries.append(.requestApprovalInfo(presentationData.theme, requestApprovalInfoText)) } @@ -538,7 +535,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: } entries.append(.timeInfo(presentationData.theme, presentationData.strings.InviteLink_Create_TimeLimitInfo)) - if !state.requestApproval || isPublic { + if !state.requestApproval { entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased())) entries.append(.usagePicker(presentationData.theme, presentationData.dateTimeFormat, state.usage, isEditingEnabled)) @@ -696,7 +693,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio |> map { presentationData, state, peer -> (ItemListControllerState, (ItemListNodeState, Any)) in let isPublic = !(peer?.addressName?.isEmpty ?? true) - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) @@ -707,7 +704,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } } - let rightNavigationButton = ItemListNavigationButton(content: .text(invite == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Save), style: state.updating ? .activity : .bold, enabled: doneIsEnabled, action: { + let rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: state.updating ? .activity : .bold, enabled: doneIsEnabled, action: { updateState { state in var updatedState = state updatedState.updating = true @@ -727,7 +724,7 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio let titleString = state.title.trimmingCharacters(in: .whitespacesAndNewlines) let title = titleString.isEmpty ? nil : titleString var usageLimit = state.usage.value - var requestNeeded: Bool? = state.requestApproval && !isPublic + var requestNeeded: Bool? = state.requestApproval if invite == nil { let subscriptionPricing: StarsSubscriptionPricing? @@ -792,14 +789,20 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio } let isGroup: Bool + let globalRequestApproval: Bool if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false + globalRequestApproval = channel.flags.contains(.requestToJoin) + } else if case let .channel(channel) = peer { + isGroup = true + globalRequestApproval = channel.flags.contains(.requestToJoin) } else { isGroup = true + globalRequestApproval = false } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(invite == nil ? presentationData.strings.InviteLink_Create_Title : presentationData.strings.InviteLink_Create_EditTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, presentationData: presentationData, configuration: configuration), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkEditControllerEntries(invite: invite, state: state, isGroup: isGroup, isPublic: isPublic, globalRequestApproval: globalRequestApproval, presentationData: presentationData, configuration: configuration), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index 580562f972..b338627fe6 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift @@ -7,14 +7,12 @@ import AsyncDisplayKit import TelegramCore import Display import AccountContext -import SolidRoundedButtonNode import ItemListUI import ItemListPeerItem import SectionHeaderItem import TelegramStringFormatting import MergeLists import ContextUI - import OverlayStatusController import PresentationDataUtils import DirectionalPanGesture diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index 25df8e383c..a5908e95b8 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift @@ -7,14 +7,12 @@ import AsyncDisplayKit import TelegramCore import Display import AccountContext -import SolidRoundedButtonNode import ItemListUI import ItemListPeerItem import SectionHeaderItem import TelegramStringFormatting import MergeLists import ContextUI - import OverlayStatusController import PresentationDataUtils import DirectionalPanGesture diff --git a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift index 5382dfed9c..04c68fdc70 100644 --- a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkItem.swift @@ -6,10 +6,11 @@ import SwiftSignalKit import AccountContext import TelegramPresentationData import ItemListUI -import SolidRoundedButtonNode import AnimatedAvatarSetNode import ShimmerEffect import TelegramCore +import ComponentFlow +import ButtonComponent private func actionButtonImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 24.0, height: 24.0), contextGenerator: { size, context in @@ -141,8 +142,8 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod private let addressButtonNode: HighlightTrackingButtonNode private let addressButtonIconNode: ASImageNode private var addressShimmerNode: ShimmerEffectNode? - private var shareButtonNode: SolidRoundedButtonNode? - private var secondaryButtonNode: SolidRoundedButtonNode? + private var shareButton: ComponentView? + private var secondaryButton: ComponentView? private let avatarsButtonNode: HighlightTrackingButtonNode private let avatarsContext: AnimatedAvatarSetContext @@ -250,11 +251,6 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod } } } - self.shareButtonNode?.pressed = { [weak self] in - if let strongSelf = self, let item = strongSelf.item { - item.shareAction?() - } - } self.avatarsButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { @@ -458,64 +454,94 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod strongSelf.referenceContainerNode.frame = strongSelf.containerNode.bounds strongSelf.addressButtonIconNode.frame = strongSelf.containerNode.bounds - let shareButtonNode: SolidRoundedButtonNode - if let currentShareButtonNode = strongSelf.shareButtonNode { - shareButtonNode = currentShareButtonNode - } else { - let buttonTheme: SolidRoundedButtonTheme - if let buttonColor = item.buttonColor { - buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) - } else { - buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) - } - shareButtonNode = SolidRoundedButtonNode(theme: buttonTheme, glass: item.systemStyle == .glass, height: buttonHeight, cornerRadius: buttonHeight * 0.5) - shareButtonNode.pressed = { [weak self] in - self?.item?.shareAction?() - } - strongSelf.addSubnode(shareButtonNode) - strongSelf.shareButtonNode = shareButtonNode - } - shareButtonNode.title = item.buttonTitle + let buttonBackgroundColor = item.buttonColor ?? item.presentationData.theme.list.itemCheckColors.fillColor + let buttonForegroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor + let buttonBackground = ButtonComponent.Background( + style: item.systemStyle == .glass ? .glass : .legacy, + color: buttonBackgroundColor, + foreground: buttonForegroundColor, + pressedColor: buttonBackgroundColor.withMultipliedAlpha(0.8), + cornerRadius: buttonHeight * 0.5 + ) - if let secondaryButtonTitle = item.secondaryButtonTitle { - let secondaryButtonNode: SolidRoundedButtonNode - if let current = strongSelf.secondaryButtonNode { - secondaryButtonNode = current - } else { - let buttonTheme: SolidRoundedButtonTheme - if let buttonColor = item.buttonColor { - buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) - } else { - buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) - } - secondaryButtonNode = SolidRoundedButtonNode(theme: buttonTheme, glass: item.systemStyle == .glass, height: buttonHeight, cornerRadius: buttonHeight * 0.5) - secondaryButtonNode.pressed = { [weak self] in - self?.item?.secondaryAction?() - } - strongSelf.addSubnode(secondaryButtonNode) - strongSelf.secondaryButtonNode = secondaryButtonNode - } - secondaryButtonNode.title = secondaryButtonTitle + let shareButton: ComponentView + if let currentShareButton = strongSelf.shareButton { + shareButton = currentShareButton } else { - if let secondaryButtonNode = strongSelf.secondaryButtonNode { - strongSelf.secondaryButtonNode = nil - secondaryButtonNode.removeFromSupernode() + shareButton = ComponentView() + strongSelf.shareButton = shareButton + } + + if item.secondaryButtonTitle != nil { + if strongSelf.secondaryButton == nil { + strongSelf.secondaryButton = ComponentView() + } + } else { + if let secondaryButton = strongSelf.secondaryButton { + strongSelf.secondaryButton = nil + secondaryButton.view?.removeFromSuperview() } } var buttonWidth = contentSize.width - leftInset - rightInset let totalButtonWidth = buttonWidth let buttonSpacing: CGFloat = 8.0 - if strongSelf.secondaryButtonNode != nil { + if strongSelf.secondaryButton != nil { buttonWidth = floor((buttonWidth - 8.0) / 2.0) } - let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate) - shareButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) + let shareButtonSize = shareButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity(id: AnyHashable(item.buttonTitle), component: AnyComponent(Text(text: item.buttonTitle, font: Font.semibold(17.0), color: buttonForegroundColor))), + isEnabled: item.enableButton, + tintWhenDisabled: false, + action: { [weak self] in + self?.item?.shareAction?() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: buttonHeight) + ) + if let shareButtonView = shareButton.view { + if shareButtonView.superview == nil { + strongSelf.view.addSubview(shareButtonView) + } + shareButtonView.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing), size: shareButtonSize) + shareButtonView.isHidden = !item.displayButton + shareButtonView.alpha = item.enableButton ? 1.0 : 0.4 + shareButtonView.isAccessibilityElement = true + shareButtonView.accessibilityLabel = item.buttonTitle + shareButtonView.accessibilityTraits = item.enableButton ? [.button] : [.button, .notEnabled] + } - if let secondaryButtonNode = strongSelf.secondaryButtonNode { - let _ = secondaryButtonNode.updateLayout(width: totalButtonWidth - buttonWidth - buttonSpacing, transition: .immediate) - secondaryButtonNode.frame = CGRect(x: leftInset + buttonWidth + buttonSpacing, y: verticalInset + fieldHeight + fieldSpacing, width: totalButtonWidth - buttonWidth - buttonSpacing, height: buttonHeight) + if let secondaryButton = strongSelf.secondaryButton, let secondaryButtonTitle = item.secondaryButtonTitle { + let secondaryButtonWidth = totalButtonWidth - buttonWidth - buttonSpacing + let secondaryButtonSize = secondaryButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity(id: AnyHashable(secondaryButtonTitle), component: AnyComponent(Text(text: secondaryButtonTitle, font: Font.semibold(17.0), color: buttonForegroundColor))), + tintWhenDisabled: false, + action: { [weak self] in + self?.item?.secondaryAction?() + } + )), + environment: {}, + containerSize: CGSize(width: secondaryButtonWidth, height: buttonHeight) + ) + if let secondaryButtonView = secondaryButton.view { + if secondaryButtonView.superview == nil { + strongSelf.view.addSubview(secondaryButtonView) + } + secondaryButtonView.frame = CGRect(origin: CGPoint(x: leftInset + buttonWidth + buttonSpacing, y: verticalInset + fieldHeight + fieldSpacing), size: secondaryButtonSize) + secondaryButtonView.isHidden = !item.displayButton + secondaryButtonView.alpha = 1.0 + secondaryButtonView.isAccessibilityElement = true + secondaryButtonView.accessibilityLabel = secondaryButtonTitle + secondaryButtonView.accessibilityTraits = [.button] + } } var totalWidth = invitedPeersLayout.size.width @@ -544,9 +570,6 @@ public class ItemListFolderInviteLinkItemNode: ListViewItemNode, ItemListItemNod strongSelf.fieldButtonNode.isUserInteractionEnabled = item.invite != nil strongSelf.addressButtonIconNode.alpha = item.invite != nil ? 1.0 : 0.0 - strongSelf.shareButtonNode?.isUserInteractionEnabled = item.enableButton - strongSelf.shareButtonNode?.alpha = item.enableButton ? 1.0 : 0.4 - strongSelf.shareButtonNode?.isHidden = !item.displayButton strongSelf.avatarsButtonNode.isHidden = !item.displayImporters strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil diff --git a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift index 6ccdd7c2b1..36238ff383 100644 --- a/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListFolderInviteLinkListItem.swift @@ -521,7 +521,7 @@ public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) if item.removeAction != nil { - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.ChatListFilter_LinkActionDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.ChatListFilter_LinkActionDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)])) } else { strongSelf.setRevealOptions((left: [], right: [])) } diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift index a00ee4df31..3ca0541df6 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkItem.swift @@ -377,7 +377,7 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { var pricingAttributedText: NSMutableAttributedString? var timerValue: TimerNode.Value? - if let invite = item.invite, case let .link(_, title, _, _, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, subscriptionPricing) = invite { + if let invite = item.invite, case let .link(_, title, _, requestApproval, _, _, date, startDate, expireDate, usageLimit, count, requestedCount, subscriptionPricing) = invite { if let title = title, !title.isEmpty { titleText = title } @@ -405,6 +405,12 @@ public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { } subtitleText += item.presentationData.strings.MemberRequests_PeopleRequestedShort(requestedCount) } + if requestApproval { + if !subtitleText.isEmpty { + subtitleText += " • " + } + subtitleText += item.presentationData.strings.InviteLink_ApprovalRequired + } if let subscriptionPricing { let text = NSMutableAttributedString() diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift index 6e389b97ed..8a1e59dea5 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift @@ -721,7 +721,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) diff --git a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift index 1761e1f681..8563d9dab8 100644 --- a/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListPermanentInviteLinkItem.swift @@ -6,13 +6,13 @@ import SwiftSignalKit import AccountContext import TelegramPresentationData import ItemListUI -import SolidRoundedButtonNode import AnimatedAvatarSetNode import ShimmerEffect import TelegramCore import Markdown import TextFormat import ComponentFlow +import ButtonComponent import MultilineTextComponent import TextNodeWithEntities @@ -143,8 +143,8 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem private let addressButtonNode: HighlightTrackingButtonNode private let addressButtonIconNode: ASImageNode private var addressShimmerNode: ShimmerEffectNode? - private var copyButtonNode: SolidRoundedButtonNode? - private var shareButtonNode: SolidRoundedButtonNode? + private var copyButton: ComponentView? + private var shareButton: ComponentView? private let avatarsButtonNode: HighlightTrackingButtonNode private let avatarsContext: AnimatedAvatarSetContext @@ -257,16 +257,6 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem } } } - self.copyButtonNode?.pressed = { [weak self] in - if let strongSelf = self, let item = strongSelf.item { - item.copyAction?() - } - } - self.shareButtonNode?.pressed = { [weak self] in - if let strongSelf = self, let item = strongSelf.item { - item.shareAction?() - } - } self.avatarsButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { @@ -535,46 +525,38 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem effectiveSeparateButtons = false } - let copyButtonNode: SolidRoundedButtonNode - if let currentCopyButtonNode = strongSelf.copyButtonNode { - copyButtonNode = currentCopyButtonNode + let buttonBackgroundColor = item.buttonColor ?? item.presentationData.theme.list.itemCheckColors.fillColor + let buttonForegroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor + let buttonBackground = ButtonComponent.Background( + style: item.systemStyle == .glass ? .glass : .legacy, + color: buttonBackgroundColor, + foreground: buttonForegroundColor, + pressedColor: buttonBackgroundColor.withMultipliedAlpha(0.8), + cornerRadius: item.systemStyle == .glass ? 26.0 : 11.0 + ) + + let copyButtonTitle = item.presentationData.strings.InviteLink_CopyShort + let copyButton: ComponentView + if let currentCopyButton = strongSelf.copyButton { + copyButton = currentCopyButton } else { - let buttonTheme: SolidRoundedButtonTheme - if let buttonColor = item.buttonColor { - buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) - } else { - buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) - } - copyButtonNode = SolidRoundedButtonNode(theme: buttonTheme, glass: item.systemStyle == .glass, height: 52.0, cornerRadius: item.systemStyle == .glass ? 26.0 : 11.0) - copyButtonNode.title = item.presentationData.strings.InviteLink_CopyShort - copyButtonNode.pressed = { [weak self] in - self?.item?.copyAction?() - } - strongSelf.addSubnode(copyButtonNode) - strongSelf.copyButtonNode = copyButtonNode + copyButton = ComponentView() + strongSelf.copyButton = copyButton } - let shareButtonNode: SolidRoundedButtonNode - if let currentShareButtonNode = strongSelf.shareButtonNode { - shareButtonNode = currentShareButtonNode + let shareButtonTitle: String + if let invite = item.invite, invitationAvailability(invite).isZero { + shareButtonTitle = item.presentationData.strings.InviteLink_ReactivateLink } else { - let buttonTheme: SolidRoundedButtonTheme - if let buttonColor = item.buttonColor { - buttonTheme = SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) - } else { - buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) - } - shareButtonNode = SolidRoundedButtonNode(theme: buttonTheme, glass: item.systemStyle == .glass, height: 52.0, cornerRadius: item.systemStyle == .glass ? 26.0 : 11.0) - if let invite = item.invite, invitationAvailability(invite).isZero { - shareButtonNode.title = item.presentationData.strings.InviteLink_ReactivateLink - } else { - shareButtonNode.title = effectiveSeparateButtons ? item.presentationData.strings.InviteLink_ShareShort : item.presentationData.strings.InviteLink_Share - } - shareButtonNode.pressed = { [weak self] in - self?.item?.shareAction?() - } - strongSelf.addSubnode(shareButtonNode) - strongSelf.shareButtonNode = shareButtonNode + shareButtonTitle = effectiveSeparateButtons ? item.presentationData.strings.InviteLink_ShareShort : item.presentationData.strings.InviteLink_Share + } + + let shareButton: ComponentView + if let currentShareButton = strongSelf.shareButton { + shareButton = currentShareButton + } else { + shareButton = ComponentView() + strongSelf.shareButton = shareButton } let buttonSpacing: CGFloat = 8.0 @@ -585,11 +567,57 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem shareButtonOriginX = leftInset + buttonWidth + buttonSpacing } - let _ = copyButtonNode.updateLayout(width: buttonWidth, transition: .immediate) - copyButtonNode.frame = CGRect(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) + let copyButtonSize = copyButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity(id: AnyHashable(copyButtonTitle), component: AnyComponent(Text(text: copyButtonTitle, font: Font.semibold(17.0), color: buttonForegroundColor))), + isEnabled: item.invite != nil, + tintWhenDisabled: false, + action: { [weak self] in + self?.item?.copyAction?() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: buttonHeight) + ) + if let copyButtonView = copyButton.view { + if copyButtonView.superview == nil { + strongSelf.view.addSubview(copyButtonView) + } + copyButtonView.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + fieldHeight + fieldSpacing), size: copyButtonSize) + copyButtonView.isHidden = !item.displayButton || !effectiveSeparateButtons + copyButtonView.alpha = item.invite != nil ? 1.0 : 0.4 + copyButtonView.isAccessibilityElement = true + copyButtonView.accessibilityLabel = copyButtonTitle + copyButtonView.accessibilityTraits = item.invite != nil ? [.button] : [.button, .notEnabled] + } - let _ = shareButtonNode.updateLayout(width: buttonWidth, transition: .immediate) - shareButtonNode.frame = CGRect(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing, width: buttonWidth, height: buttonHeight) + let shareButtonSize = shareButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity(id: AnyHashable(shareButtonTitle), component: AnyComponent(Text(text: shareButtonTitle, font: Font.semibold(17.0), color: buttonForegroundColor))), + isEnabled: item.invite != nil, + tintWhenDisabled: false, + action: { [weak self] in + self?.item?.shareAction?() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: buttonHeight) + ) + if let shareButtonView = shareButton.view { + if shareButtonView.superview == nil { + strongSelf.view.addSubview(shareButtonView) + } + shareButtonView.frame = CGRect(origin: CGPoint(x: shareButtonOriginX, y: verticalInset + fieldHeight + fieldSpacing), size: shareButtonSize) + shareButtonView.isHidden = !item.displayButton + shareButtonView.alpha = item.invite != nil ? 1.0 : 0.4 + shareButtonView.isAccessibilityElement = true + shareButtonView.accessibilityLabel = shareButtonTitle + shareButtonView.accessibilityTraits = item.invite != nil ? [.button] : [.button, .notEnabled] + } if let justCreatedCallTextNodeLayout { if let justCreatedCallTextNode = justCreatedCallTextNodeLayout.1(TextNodeWithEntities.Arguments( @@ -608,7 +636,8 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem justCreatedCallTextNode.textNode.view.addGestureRecognizer(UITapGestureRecognizer(target: strongSelf, action: #selector(strongSelf.justCreatedCallTextTap(_:)))) } - let justCreatedCallTextNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - justCreatedCallTextNodeLayout.0.size.width) / 2.0), y: shareButtonNode.frame.maxY + justCreatedCallTextSpacing), size: CGSize(width: justCreatedCallTextNodeLayout.0.size.width, height: justCreatedCallTextNodeLayout.0.size.height)) + let buttonMaxY = verticalInset + fieldHeight + fieldSpacing + buttonHeight + let justCreatedCallTextNodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - justCreatedCallTextNodeLayout.0.size.width) / 2.0), y: buttonMaxY + justCreatedCallTextSpacing), size: CGSize(width: justCreatedCallTextNodeLayout.0.size.width, height: justCreatedCallTextNodeLayout.0.size.height)) justCreatedCallTextNode.textNode.frame = justCreatedCallTextNodeFrame let justCreatedCallSeparatorText: ComponentView @@ -648,7 +677,7 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem environment: {}, containerSize: CGSize(width: params.width - leftInset - rightInset, height: 100.0) ) - let justCreatedCallSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((params.width - justCreatedCallSeparatorTextSize.width) * 0.5), y: shareButtonNode.frame.maxY + justCreatedCallSeparatorSpacing), size: justCreatedCallSeparatorTextSize) + let justCreatedCallSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((params.width - justCreatedCallSeparatorTextSize.width) * 0.5), y: buttonMaxY + justCreatedCallSeparatorSpacing), size: justCreatedCallSeparatorTextSize) if let justCreatedCallSeparatorTextView = justCreatedCallSeparatorText.view { if justCreatedCallSeparatorTextView.superview == nil { strongSelf.view.addSubview(justCreatedCallSeparatorTextView) @@ -703,14 +732,6 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem strongSelf.addressButtonIconNode.alpha = item.invite != nil ? 1.0 : 0.0 - strongSelf.copyButtonNode?.isUserInteractionEnabled = item.invite != nil - strongSelf.copyButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4 - strongSelf.copyButtonNode?.isHidden = !item.displayButton || !effectiveSeparateButtons - - strongSelf.shareButtonNode?.isUserInteractionEnabled = item.invite != nil - strongSelf.shareButtonNode?.alpha = item.invite != nil ? 1.0 : 0.4 - strongSelf.shareButtonNode?.isHidden = !item.displayButton - strongSelf.avatarsButtonNode.isHidden = !item.displayImporters strongSelf.avatarsNode.isHidden = !item.displayImporters || item.invite == nil strongSelf.invitedPeersNode.isHidden = !item.displayImporters || item.invite == nil diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 11e9c2d5a2..e1f339963a 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -321,7 +321,7 @@ public struct ItemListPeerItemShimmering { } } -public final class ItemListPeerItem: ListViewItem, ItemListItem { +public final class ItemListPeerItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem { public enum Context { public final class Custom { public let accountPeerId: EnginePeer.Id @@ -468,6 +468,10 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { let disableInteractiveTransitionIfNecessary: Bool let storyStats: EnginePeerStoryStats? let openStories: ((UIView) -> Void)? + + public var hasActiveRevealOptions: Bool { + return self.editing.revealed == true + } public init( presentationData: ItemListPresentationData, @@ -1013,12 +1017,12 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo color = item.presentationData.theme.list.itemDisclosureActions.accent.fillColor textColor = item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor } - mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor)) + mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, iconColor: textColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)) index += 1 } peerRevealOptions = mappedOptions } else { - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)] } } else { peerRevealOptions = [] @@ -1434,26 +1438,29 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo let hasCorners = itemListHasRoundedBlockLayout(params) && !item.noCorners var hasTopCorners = false var hasBottomCorners = false + let topStripeIsHidden: Bool switch neighbors.top { case .sameSection(false): - strongSelf.topStripeNode.isHidden = true + topStripeIsHidden = true default: hasTopCorners = true - strongSelf.topStripeNode.isHidden = !item.displayDecorations || hasCorners || !item.hasTopStripe + topStripeIsHidden = !item.displayDecorations || hasCorners || !item.hasTopStripe } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat + let bottomStripeIsHidden: Bool switch neighbors.bottom { case .sameSection(false): bottomStripeInset = leftInset + editingOffset bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = !item.displayDecorations + bottomStripeIsHidden = !item.displayDecorations default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners || !item.displayDecorations + bottomStripeIsHidden = hasCorners || !item.displayDecorations } + strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil @@ -1778,7 +1785,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) + strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition) if let presence = item.presence { strongSelf.peerPresenceManager?.reset(presence: presence) @@ -1860,7 +1867,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo var isHighlighted = false var reallyHighlighted: Bool { - var reallyHighlighted = self.isHighlighted + var reallyHighlighted = self.isHighlighted || self.isRevealOptionsActive if let (item, _, _, _) = self.layoutParams, item.highlighted { reallyHighlighted = true } @@ -1868,39 +1875,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } func updateIsHighlighted(transition: ContainedViewLayoutTransition) { - if self.reallyHighlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if transition.isAnimated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.reallyHighlighted, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) } override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { @@ -2002,6 +1977,12 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo transition.updateFrame(view: avatarIconComponentView, frame: threadIconFrame) } } + + override public func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateIsHighlighted(transition: transition) + } override public func revealOptionsInteractivelyOpened() { if let (item, _, _, _) = self.layoutParams { diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 73a61b8757..daf52b4de4 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -36,7 +36,7 @@ public enum ItemListStickerPackItemControl: Equatable { case check(checked: Bool) } -public final class ItemListStickerPackItem: ListViewItem, ItemListItem { +public final class ItemListStickerPackItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem { let presentationData: ItemListPresentationData let context: AccountContext let systemStyle: ItemListSystemStyle @@ -56,6 +56,10 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem { let removePack: () -> Void let toggleSelected: () -> Void + public var hasActiveRevealOptions: Bool { + return self.editing.revealed + } + public init(presentationData: ItemListPresentationData, context: AccountContext, systemStyle: ItemListSystemStyle = .legacy, packInfo: StickerPackCollectionInfo.Accessor, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, playAnimatedStickers: Bool, style: ItemListStyle = .blocks, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (EngineItemCollectionId?, EngineItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void, toggleSelected: @escaping () -> Void) { self.presentationData = presentationData self.context = context @@ -180,6 +184,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { private let activateArea: AccessibilityAreaNode private let fetchDisposable = MetaDisposable() + private var isHighlighted: Bool = false override var canBeSelected: Bool { if self.selectableControlNode != nil || self.editableControlNode != nil || self.disabledOverlayNode != nil { @@ -372,7 +377,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let packRevealOptions: [ItemListRevealOption] if item.editing.editable && item.enabled && !item.editing.editing { - packRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] + packRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)] } else { packRevealOptions = [] } @@ -410,14 +415,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let leftInset: CGFloat = 65.0 + params.leftInset - let verticalInset: CGFloat - switch item.systemStyle { - case .glass: - verticalInset = 13.0 - case .legacy: - verticalInset = 11.0 - } - + let verticalInset: CGFloat = 11.0 let titleSpacing: CGFloat = 2.0 let separatorHeight = UIScreenPixel @@ -764,26 +762,29 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false + let topStripeIsHidden: Bool switch neighbors.top { case .sameSection(false): - strongSelf.topStripeNode.isHidden = true + topStripeIsHidden = true default: hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners + topStripeIsHidden = hasCorners } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat + let bottomStripeIsHidden: Bool switch neighbors.bottom { case .sameSection(false): bottomStripeInset = leftInset + editingOffset bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false + bottomStripeIsHidden = false default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners + bottomStripeIsHidden = hasCorners } + strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil @@ -877,7 +878,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { strongSelf.imageNode.setSignal(updatedImageSignal) } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) + strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)), transition: transition) strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) @@ -895,39 +896,8 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } + self.isHighlighted = highlighted + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) } override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { @@ -967,6 +937,12 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: animationNode, frame: CGRect(origin: CGPoint(x: params.leftInset + self.revealOffset + editingOffset + 15.0 + floor((boundingSize.width - animationNode.frame.size.width) / 2.0), y: animationNode.frame.minY), size: animationNode.frame.size)) } } + + override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) + } override func revealOptionsInteractivelyOpened() { if let (item, _, _) = self.layoutParams { diff --git a/submodules/ItemListUI/BUILD b/submodules/ItemListUI/BUILD index cbd9e07683..0b641c47f8 100644 --- a/submodules/ItemListUI/BUILD +++ b/submodules/ItemListUI/BUILD @@ -35,6 +35,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/TelegramUI/Components/ListItemComponentAdaptor", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/GlassControls", "//submodules/TelegramUI/Components/HorizontalTabsComponent", ], visibility = [ diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index dbc2384f58..9b83218c2d 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -25,6 +25,8 @@ public enum ItemListNavigationButtonContentIcon { case search case add case action + case close + case done } public enum ItemListNavigationButtonContent: Equatable { @@ -426,16 +428,27 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable case let .text(value): item = UIBarButtonItem(title: value, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) case let .icon(icon): - var image: UIImage? - switch icon { + if [.close, .done].contains(icon) { + switch icon { + case .done: + item = UIBarButtonItem(title: "___done", style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) + default: + item = UIBarButtonItem(title: "___close", style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) + } + } else { + var image: UIImage? + switch icon { case .search: image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.presentationData.theme) case .add: image = PresentationResourcesRootController.navigationAddIcon(controllerState.presentationData.theme) case .action: image = PresentationResourcesRootController.navigationShareIcon(controllerState.presentationData.theme) + default: + image = nil + } + item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) } - item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) case let .node(node): item = UIBarButtonItem(customDisplayNode: node) item.setCustomAction({ [weak self] in @@ -488,16 +501,27 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable case let .text(value): item = UIBarButtonItem(title: value, style: style.barButtonItemStyle, target: strongSelf, action: action) case let .icon(icon): - var image: UIImage? - switch icon { + if [.close, .done].contains(icon) { + switch icon { + case .done: + item = UIBarButtonItem(title: "___done", style: style.barButtonItemStyle, target: strongSelf, action: action) + default: + item = UIBarButtonItem(title: "___close", style: style.barButtonItemStyle, target: strongSelf, action: action) + } + } else { + var image: UIImage? + switch icon { case .search: image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.presentationData.theme) case .add: image = PresentationResourcesRootController.navigationAddIcon(controllerState.presentationData.theme) case .action: image = PresentationResourcesRootController.navigationShareIcon(controllerState.presentationData.theme) + default: + image = nil + } + item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action) } - item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action) case let .node(node): item = UIBarButtonItem(customDisplayNode: node) item.setCustomAction({ [weak self] in @@ -665,6 +689,16 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } } + public func itemNode(forTag tag: ItemListItemTag) -> ListViewItemNode? { + var result: ListViewItemNode? + self.forEachItemNode { itemNode in + if result == nil, let taggedItemNode = itemNode as? ItemListItemNode, let itemTag = taggedItemNode.tag, itemTag.isEqual(to: tag) { + result = itemNode + } + } + return result + } + public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, atTop: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) { self.controllerNode.listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve) } diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 76901d6706..6b9efaf342 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -6,6 +6,8 @@ import SwiftSignalKit import TelegramCore import TelegramPresentationData import MergeLists +import ComponentFlow +import GlassControls public protocol ItemListHeaderItemNode: AnyObject { func updateTheme(theme: PresentationTheme) @@ -255,7 +257,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { private var emptyStateItem: ItemListControllerEmptyStateItem? private var emptyStateNode: ItemListControllerEmptyStateItemNode? - private var toolbarNode: ToolbarNode? + private var toolbar: ComponentView? private var searchItem: ItemListControllerSearch? private var searchNode: ItemListControllerSearchNode? @@ -654,7 +656,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { insets.bottom = max(insets.bottom, additionalInsets.bottom) let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { insets.left += inset insets.right += inset } @@ -666,60 +668,165 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self.listNodeContainer.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode) } - if let toolbarItem = self.toolbarItem { - var tabBarHeight: CGFloat - let bottomInset: CGFloat = insets.bottom - if !layout.safeInsets.left.isZero { - tabBarHeight = 34.0 + bottomInset - insets.bottom += 34.0 + if let toolbarData = self.toolbarItem, let theme = self.theme { + var panelsBottomInset: CGFloat = layout.insets(options: []).bottom + if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 { + panelsBottomInset = inputHeight + 8.0 + } + if panelsBottomInset == 0.0 { + panelsBottomInset = 8.0 } else { - tabBarHeight = 49.0 + bottomInset - insets.bottom += 49.0 + panelsBottomInset = max(panelsBottomInset, 8.0) } - let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) + let sideInset: CGFloat = 20.0 + let toolbarHeight = 44.0 + let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight)) - if let toolbarNode = self.toolbarNode { - transition.updateFrame(node: toolbarNode, frame: toolbarFrame) - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: transition) - } else if let theme = self.theme { - let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: theme), displaySeparator: true) - toolbarNode.frame = toolbarFrame - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: .immediate) - self.addSubnode(toolbarNode) - self.toolbarNode = toolbarNode - if case let .animated(duration, curve) = transition { - toolbarNode.layer.animatePosition(from: CGPoint(x: 0.0, y: toolbarFrame.height), to: CGPoint(), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, additive: true) + let toolbar: ComponentView + var toolbarTransition = ComponentTransition(transition) + if let current = self.toolbar { + toolbar = current + } else { + toolbar = ComponentView() + self.toolbar = toolbar + toolbarTransition = .immediate + } + + let _ = toolbar.update( + transition: toolbarTransition, + component: AnyComponent(GlassControlPanelComponent( + theme: theme, + leftItem: toolbarData.toolbar.leftAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "left_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self, let toolbarData = self.toolbarItem else { + return + } + toolbarData.actions[0].action() + } : nil + )], + background: .panel + ) + }, + centralItem: toolbarData.toolbar.middleAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "right_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self, let toolbarData = self.toolbarItem else { + return + } + if toolbarData.actions.count == 1 { + toolbarData.actions[0].action() + } else if toolbarData.actions.count == 3 { + toolbarData.actions[1].action() + } + } : nil + )], + background: .panel + ) + }, + rightItem: toolbarData.toolbar.rightAction.flatMap { value in + return GlassControlPanelComponent.Item( + items: [GlassControlGroupComponent.Item( + id: "right_" + value.title, + content: .text(value.title), + action: value.isEnabled ? { [weak self] in + guard let self, let toolbarData = self.toolbarItem else { + return + } + if toolbarData.actions.count == 2 { + toolbarData.actions[1].action() + } else if toolbarData.actions.count == 3 { + toolbarData.actions[2].action() + } + } : nil + )], + background: .panel + ) + }, + centerAlignmentIfPossible: true + )), + environment: {}, + containerSize: toolbarFrame.size + ) + + if let toolbarView = toolbar.view { + if toolbarView.superview == nil { + self.view.addSubview(toolbarView) + toolbarView.alpha = 0.0 } + toolbarTransition.setFrame(view: toolbarView, frame: toolbarFrame) + ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 1.0) } - - self.toolbarNode?.left = { - toolbarItem.actions[0].action() - } - self.toolbarNode?.right = { - if toolbarItem.actions.count == 2 { - toolbarItem.actions[1].action() - } else if toolbarItem.actions.count == 3 { - toolbarItem.actions[2].action() - } - } - self.toolbarNode?.middle = { - if toolbarItem.actions.count == 1 { - toolbarItem.actions[0].action() - } else if toolbarItem.actions.count == 3 { - toolbarItem.actions[1].action() - } - } - } else if let toolbarNode = self.toolbarNode { - self.toolbarNode = nil - if case let .animated(duration, curve) = transition { - toolbarNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: toolbarNode.frame.size.height), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, additive: true, completion: { [weak toolbarNode] _ in - toolbarNode?.removeFromSupernode() + } else if let toolbar = self.toolbar { + self.toolbar = nil + if let toolbarView = toolbar.view { + ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 0.0, completion: { [weak toolbarView] _ in + toolbarView?.removeFromSuperview() }) - } else { - toolbarNode.removeFromSupernode() } } + +// if let toolbarItem = self.toolbarItem { +// var tabBarHeight: CGFloat +// let bottomInset: CGFloat = insets.bottom +// if !layout.safeInsets.left.isZero { +// tabBarHeight = 34.0 + bottomInset +// insets.bottom += 34.0 +// } else { +// tabBarHeight = 49.0 + bottomInset +// insets.bottom += 49.0 +// } +// +// let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight)) +// +// if let toolbarNode = self.toolbarNode { +// transition.updateFrame(node: toolbarNode, frame: toolbarFrame) +// toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: transition) +// } else if let theme = self.theme { +// let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: theme), displaySeparator: true) +// toolbarNode.frame = toolbarFrame +// toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: .immediate) +// self.addSubnode(toolbarNode) +// self.toolbarNode = toolbarNode +// if case let .animated(duration, curve) = transition { +// toolbarNode.layer.animatePosition(from: CGPoint(x: 0.0, y: toolbarFrame.height), to: CGPoint(), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, additive: true) +// } +// } +// +// self.toolbarNode?.left = { +// toolbarItem.actions[0].action() +// } +// self.toolbarNode?.right = { +// if toolbarItem.actions.count == 2 { +// toolbarItem.actions[1].action() +// } else if toolbarItem.actions.count == 3 { +// toolbarItem.actions[2].action() +// } +// } +// self.toolbarNode?.middle = { +// if toolbarItem.actions.count == 1 { +// toolbarItem.actions[0].action() +// } else if toolbarItem.actions.count == 3 { +// toolbarItem.actions[1].action() +// } +// } +// } else if let toolbarNode = self.toolbarNode { +// self.toolbarNode = nil +// if case let .animated(duration, curve) = transition { +// toolbarNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: toolbarNode.frame.size.height), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, additive: true, completion: { [weak toolbarNode] _ in +// toolbarNode?.removeFromSupernode() +// }) +// } else { +// toolbarNode.removeFromSupernode() +// } +// } if let headerItemNode = self.headerItemNode { let headerHeight = headerItemNode.updateLayout(layout: layout, transition: transition) @@ -977,7 +1084,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { insets.bottom = footerHeight let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { insets.left += inset insets.right += inset } diff --git a/submodules/ItemListUI/Sources/ItemListItem.swift b/submodules/ItemListUI/Sources/ItemListItem.swift index bbf501135c..6f4f3d824b 100644 --- a/submodules/ItemListUI/Sources/ItemListItem.swift +++ b/submodules/ItemListUI/Sources/ItemListItem.swift @@ -15,6 +15,10 @@ public protocol ItemListItem { var requestsNoInset: Bool { get } } +public protocol ItemListRevealOptionsStatefulItem: ItemListItem { + var hasActiveRevealOptions: Bool { get } +} + public extension ItemListItem { //let accessoryItem: ListViewAccessoryItem? @@ -62,10 +66,14 @@ public enum ItemListNeighbor { public struct ItemListNeighbors { public var top: ItemListNeighbor public var bottom: ItemListNeighbor + public var topHasActiveRevealOptions: Bool + public var bottomHasActiveRevealOptions: Bool - public init(top: ItemListNeighbor, bottom: ItemListNeighbor) { + public init(top: ItemListNeighbor, bottom: ItemListNeighbor, topHasActiveRevealOptions: Bool = false, bottomHasActiveRevealOptions: Bool = false) { self.top = top self.bottom = bottom + self.topHasActiveRevealOptions = topHasActiveRevealOptions + self.bottomHasActiveRevealOptions = bottomHasActiveRevealOptions } } @@ -108,7 +116,12 @@ public func itemListNeighbors(item: ItemListItem, topItem: ItemListItem?, bottom bottomNeighbor = .none } - return ItemListNeighbors(top: topNeighbor, bottom: bottomNeighbor) + return ItemListNeighbors( + top: topNeighbor, + bottom: bottomNeighbor, + topHasActiveRevealOptions: (topItem as? ItemListRevealOptionsStatefulItem)?.hasActiveRevealOptions ?? false, + bottomHasActiveRevealOptions: (bottomItem as? ItemListRevealOptionsStatefulItem)?.hasActiveRevealOptions ?? false + ) } public func itemListNeighborsPlainInsets(_ neighbors: ItemListNeighbors) -> UIEdgeInsets { @@ -164,7 +177,7 @@ public func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors, _ par } public func itemListHasRoundedBlockLayout(_ params: ListViewItemLayoutParams) -> Bool { - return params.width >= 350.0 + return params.width >= 320.0 } public final class ItemListPresentationData: Equatable { diff --git a/submodules/ItemListUI/Sources/ItemListRevealOptionsNode.swift b/submodules/ItemListUI/Sources/ItemListRevealOptionsNode.swift index 0554cbac28..654626b0cd 100644 --- a/submodules/ItemListUI/Sources/ItemListRevealOptionsNode.swift +++ b/submodules/ItemListUI/Sources/ItemListRevealOptionsNode.swift @@ -8,7 +8,7 @@ public enum ItemListRevealOptionIcon: Equatable { case none case image(image: UIImage) case animation(animation: String, scale: CGFloat, offset: CGFloat, replaceColors: [UInt32]?, flip: Bool) - + public static func ==(lhs: ItemListRevealOptionIcon, rhs: ItemListRevealOptionIcon) -> Bool { switch lhs { case .none: @@ -38,16 +38,18 @@ public struct ItemListRevealOption: Equatable { public let title: String public let icon: ItemListRevealOptionIcon public let color: UIColor + public let iconColor: UIColor public let textColor: UIColor - - public init(key: Int32, title: String, icon: ItemListRevealOptionIcon, color: UIColor, textColor: UIColor) { + + public init(key: Int32, title: String, icon: ItemListRevealOptionIcon, color: UIColor, iconColor: UIColor, textColor: UIColor) { self.key = key self.title = title self.icon = icon self.color = color + self.iconColor = iconColor self.textColor = textColor } - + public static func ==(lhs: ItemListRevealOption, rhs: ItemListRevealOption) -> Bool { if lhs.key != rhs.key { return false @@ -58,6 +60,9 @@ public struct ItemListRevealOption: Equatable { if !lhs.color.isEqual(rhs.color) { return false } + if !lhs.iconColor.isEqual(rhs.iconColor) { + return false + } if !lhs.textColor.isEqual(rhs.textColor) { return false } @@ -68,45 +73,128 @@ public struct ItemListRevealOption: Equatable { } } -private let titleFontWithIcon = Font.medium(13.0) -private let titleFontWithoutIcon = Font.regular(17.0) +private let titleFont = Font.regular(11.0) +private let iconlessTitleFont = Font.regular(13.0) -private enum ItemListRevealOptionAlignment { - case left - case right +private let optionSpacing: CGFloat = 10.0 +private let optionEdgeInset: CGFloat = 10.0 +private let optionTitleSpacing: CGFloat = 4.0 +private let optionRevealStartOverlap: CGFloat = 12.0 +private let optionRevealEndDistance: CGFloat = 10.0 +private let optionExpandedActivationWidthFactor: CGFloat = 3.0 +private let optionExpandedTransitionDistance: CGFloat = 16.0 +private let optionIconlessTitleHorizontalInset: CGFloat = 10.0 +private let optionIconAnimationResponse: CGFloat = 18.0 +private let optionIconAnimationSnapDistance: CGFloat = 0.5 + +private extension ItemListRevealOptionIcon { + var hasVisualIcon: Bool { + switch self { + case .none: + return false + case .image, .animation: + return true + } + } +} + +private struct ItemListRevealOptionLayoutMetrics { + let shapeSize: CGSize + let slotWidth: CGFloat + let titleWidth: CGFloat + let iconMaxSide: CGFloat + let cornerRadius: CGFloat + let expandedIconInset: CGFloat + + var contentHeight: CGFloat { + return self.shapeSize.height + optionTitleSpacing + ceil(titleFont.lineHeight) + } + + var slotShapeInset: CGFloat { + return floor((self.slotWidth - self.shapeSize.width) / 2.0) + } + + static func metrics(for height: CGFloat, hasVisualIcons: Bool) -> ItemListRevealOptionLayoutMetrics { + let regularShapeSize = CGSize(width: 50.0, height: 50.0) + let compactShapeSize = CGSize(width: 60.0, height: 32.0) + let regularContentHeight = regularShapeSize.height + optionTitleSpacing + ceil(titleFont.lineHeight) + if height < regularContentHeight || !hasVisualIcons { + return ItemListRevealOptionLayoutMetrics(shapeSize: compactShapeSize, slotWidth: 70.0, titleWidth: 70.0, iconMaxSide: 20.0, cornerRadius: 16.0, expandedIconInset: 16.0) + } else { + return ItemListRevealOptionLayoutMetrics(shapeSize: regularShapeSize, slotWidth: 60.0, titleWidth: 60.0, iconMaxSide: 40.0, cornerRadius: 25.0, expandedIconInset: 20.0) + } + } + + func revealWidth(count: Int) -> CGFloat { + if count == 0 { + return 0.0 + } + return optionEdgeInset * 2.0 + self.shapeSize.width * CGFloat(count) + optionSpacing * CGFloat(count - 1) + } +} + +private func clampToUnitInterval(_ value: CGFloat) -> CGFloat { + return max(0.0, min(1.0, value)) +} + +private func frameCenter(_ frame: CGRect) -> CGPoint { + return CGPoint(x: frame.midX, y: frame.midY) } private final class ItemListRevealOptionNode: ASDisplayNode { + private let contentContainerNode: ASDisplayNode private let backgroundNode: ASDisplayNode private let highlightNode: ASDisplayNode private let titleNode: ASTextNode private let iconNode: ASImageNode? private let animationNode: SimpleAnimationNode? - + private let enableAnimations: Bool - + private let displaysTitleInsidePill: Bool + private var animationScale: CGFloat = 1.0 private var animationNodeOffset: CGFloat = 0.0 private var animationNodeFlip = false - var alignment: ItemListRevealOptionAlignment? + + private var iconAnimationLink: SharedDisplayLinkDriver.Link? + private weak var manuallyAnimatedIconNode: ASDisplayNode? + private var currentIconCenter: CGPoint? + private var targetIconCenter: CGPoint? + + private var didApplyLayout = false var isExpanded: Bool = false - - init(title: String, icon: ItemListRevealOptionIcon, color: UIColor, textColor: UIColor, enableAnimations: Bool) { + + var hasAppliedLayout: Bool { + return self.didApplyLayout + } + + init(title: String, icon: ItemListRevealOptionIcon, color: UIColor, iconColor: UIColor, textColor: UIColor, enableAnimations: Bool) { + self.contentContainerNode = ASDisplayNode() self.backgroundNode = ASDisplayNode() self.highlightNode = ASDisplayNode() - + self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: title, font: icon == .none ? titleFontWithoutIcon : titleFontWithIcon, textColor: textColor) - + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + + let displaysTitleInsidePill: Bool + if case .none = icon { + displaysTitleInsidePill = true + } else { + displaysTitleInsidePill = false + } + self.displaysTitleInsidePill = displaysTitleInsidePill + self.titleNode.attributedText = NSAttributedString(string: title, font: displaysTitleInsidePill ? iconlessTitleFont : titleFont, textColor: displaysTitleInsidePill ? iconColor : textColor) + self.enableAnimations = enableAnimations - + switch icon { case let .image(image): let iconNode = ASImageNode() - iconNode.image = generateTintedImage(image: image, color: textColor) + iconNode.image = generateTintedImage(image: image, color: iconColor) self.iconNode = iconNode self.animationNode = nil - + case let .animation(animation, scale, offset, replaceColors, flip): self.animationScale = scale self.iconNode = nil @@ -116,7 +204,7 @@ private final class ItemListRevealOptionNode: ASDisplayNode { colors[colorToReplace] = color.rgb } } - self.animationNode = SimpleAnimationNode(animationName: animation, replaceColors: colors, size: CGSize(width: 79.0, height: 79.0), playOnce: true) + self.animationNode = SimpleAnimationNode(animationName: animation, replaceColors: colors, size: CGSize(width: 66.0, height: 66.0), playOnce: true) if !enableAnimations { self.animationNode!.seekToEnd() } @@ -126,28 +214,34 @@ private final class ItemListRevealOptionNode: ASDisplayNode { self.animationNodeOffset = offset self.animationNodeFlip = flip break - + case .none: self.iconNode = nil self.animationNode = nil } - + super.init() - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.titleNode) + + self.contentContainerNode.layer.allowsGroupOpacity = true + self.addSubnode(self.contentContainerNode) + self.contentContainerNode.addSubnode(self.backgroundNode) + self.contentContainerNode.addSubnode(self.titleNode) if let iconNode = self.iconNode { - self.addSubnode(iconNode) + self.contentContainerNode.addSubnode(iconNode) } else if let animationNode = self.animationNode { - self.addSubnode(animationNode) + self.contentContainerNode.addSubnode(animationNode) } self.backgroundNode.backgroundColor = color self.highlightNode.backgroundColor = color.withMultipliedBrightnessBy(0.9) } - + + deinit { + self.stopManualIconAnimation() + } + func setHighlighted(_ highlighted: Bool) { if highlighted { - self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode) + self.contentContainerNode.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode) self.highlightNode.layer.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3) self.highlightNode.alpha = 1.0 } else { @@ -155,145 +249,264 @@ private final class ItemListRevealOptionNode: ASDisplayNode { self.highlightNode.alpha = 0.0 } } - + func resetAnimation() { self.animationNode?.reset() + self.stopManualIconAnimation() } - - func updateLayout(isFirst: Bool, isLeft: Bool, baseSize: CGSize, alignment: ItemListRevealOptionAlignment, isExpanded: Bool, extendedWidth: CGFloat, sideInset: CGFloat, transition: ContainedViewLayoutTransition, additive: Bool, revealFactor: CGFloat, animateIconMovement: Bool) { - self.highlightNode.frame = CGRect(origin: CGPoint(), size: baseSize) - - var animateAdditive = false - if additive && transition.isAnimated && self.isExpanded != isExpanded { - animateAdditive = true + + private func currentIconPresentationCenter(iconNode: ASDisplayNode) -> CGPoint { + return iconNode.layer.presentation()?.position ?? iconNode.position + } + + private func isManualIconAnimationAtTarget(center: CGPoint) -> Bool { + guard let targetIconCenter = self.targetIconCenter else { + return true } - + let centerDeltaX = targetIconCenter.x - center.x + let centerDeltaY = targetIconCenter.y - center.y + let centerDistance = sqrt(centerDeltaX * centerDeltaX + centerDeltaY * centerDeltaY) + return centerDistance <= optionIconAnimationSnapDistance + } + + private func stopManualIconAnimation() { + self.iconAnimationLink?.isPaused = true + self.iconAnimationLink?.invalidate() + self.iconAnimationLink = nil + self.manuallyAnimatedIconNode = nil + self.currentIconCenter = nil + self.targetIconCenter = nil + } + + private func updateManualIconCenter(iconNode: ASDisplayNode, targetCenter: CGPoint, forceImmediate: Bool) { + iconNode.layer.removeAnimation(forKey: "position") + + if self.manuallyAnimatedIconNode !== iconNode || self.currentIconCenter == nil { + self.currentIconCenter = self.currentIconPresentationCenter(iconNode: iconNode) + self.manuallyAnimatedIconNode = iconNode + } + + self.targetIconCenter = targetCenter + + if forceImmediate { + iconNode.position = targetCenter + self.stopManualIconAnimation() + return + } + + if let currentIconCenter = self.currentIconCenter, self.isManualIconAnimationAtTarget(center: currentIconCenter) { + iconNode.position = targetCenter + self.stopManualIconAnimation() + return + } + + if self.iconAnimationLink == nil { + self.iconAnimationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in + self?.tickManualIconAnimation(deltaTime: deltaTime) + }) + self.iconAnimationLink?.isPaused = false + } + } + + private func tickManualIconAnimation(deltaTime: CGFloat) { + guard let iconNode = self.manuallyAnimatedIconNode, let currentIconCenter = self.currentIconCenter, let targetIconCenter = self.targetIconCenter else { + self.stopManualIconAnimation() + return + } + + let clampedDeltaTime = min(0.05, max(0.0, deltaTime)) + let progress = 1.0 - exp(-clampedDeltaTime * optionIconAnimationResponse) + let updatedCenter = CGPoint( + x: currentIconCenter.x + (targetIconCenter.x - currentIconCenter.x) * progress, + y: currentIconCenter.y + (targetIconCenter.y - currentIconCenter.y) * progress + ) + + if self.isManualIconAnimationAtTarget(center: updatedCenter) { + iconNode.position = targetIconCenter + self.stopManualIconAnimation() + } else { + self.currentIconCenter = updatedCenter + iconNode.position = updatedCenter + } + } + + func updateLayout(isLeft: Bool, isPrimary: Bool, metrics: ItemListRevealOptionLayoutMetrics, revealProgress: CGFloat, overswipeProgress: CGFloat, expandedProgress: CGFloat, isStretched: Bool, isExpanded: Bool, transition: ContainedViewLayoutTransition) { + let didApplyLayout = self.didApplyLayout + let bounds = CGRect(origin: CGPoint(), size: self.bounds.size) + transition.updateFrame(node: self.contentContainerNode, frame: bounds) + + let titleSize = self.titleNode.measure(CGSize(width: metrics.titleWidth, height: CGFloat.greatestFiniteMagnitude)) + let pillSize: CGSize + if self.displaysTitleInsidePill { + let pillWidth = max(metrics.shapeSize.width, min(metrics.slotWidth, titleSize.width + optionIconlessTitleHorizontalInset * 2.0)) + pillSize = CGSize(width: pillWidth, height: metrics.shapeSize.height) + } else { + pillSize = metrics.shapeSize + } + let shapeY: CGFloat + if self.displaysTitleInsidePill { + shapeY = floor((bounds.height - pillSize.height) / 2.0) + } else { + shapeY = floor((bounds.height - metrics.contentHeight) / 2.0) + } + + let shapeFrameX: CGFloat + if isStretched { + shapeFrameX = isLeft ? 0.0 : bounds.width - pillSize.width + } else { + shapeFrameX = floor((metrics.slotWidth - pillSize.width) / 2.0) + } + let shapeFrame = CGRect(origin: CGPoint(x: shapeFrameX, y: shapeY), size: pillSize) let backgroundFrame: CGRect - if isFirst { - backgroundFrame = CGRect(origin: CGPoint(x: isLeft ? -400.0 : 0.0, y: 0.0), size: CGSize(width: extendedWidth + 400.0, height: baseSize.height)) + if isStretched { + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: shapeY), size: CGSize(width: bounds.width, height: pillSize.height)) } else { - backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: extendedWidth, height: baseSize.height)) + backgroundFrame = shapeFrame } - let deltaX: CGFloat - if animateAdditive { - let previousFrame = self.backgroundNode.frame - self.backgroundNode.frame = backgroundFrame - if isLeft { - deltaX = previousFrame.width - backgroundFrame.width - } else { - deltaX = -(previousFrame.width - backgroundFrame.width) - } - if !animateIconMovement { - transition.animatePositionAdditive(node: self.backgroundNode, offset: CGPoint(x: deltaX, y: 0.0)) - } - } else { - deltaX = 0.0 - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - } - - self.alignment = alignment + + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.highlightNode, frame: backgroundFrame) + transition.updateCornerRadius(node: self.backgroundNode, cornerRadius: metrics.cornerRadius) + transition.updateCornerRadius(node: self.highlightNode, cornerRadius: metrics.cornerRadius) + + let wasExpanded = self.isExpanded self.isExpanded = isExpanded - let titleSize = self.titleNode.calculatedSize - var contentRect = CGRect(origin: CGPoint(), size: baseSize) - switch alignment { - case .left: - contentRect.origin.x = 0.0 - case .right: - contentRect.origin.x = extendedWidth - contentRect.width + self.didApplyLayout = true + let contentAlpha: CGFloat + if isPrimary { + contentAlpha = revealProgress + } else { + contentAlpha = revealProgress * (1.0 - 0.3 * overswipeProgress) } - + let contentScale = 0.3 + 0.7 * revealProgress + transition.updateAlpha(node: self.contentContainerNode, alpha: contentAlpha) + transition.updateTransform(node: self.contentContainerNode, transform: CGAffineTransform(scaleX: contentScale, y: contentScale)) + + let titleAlpha: CGFloat = isPrimary && !self.displaysTitleInsidePill ? (1.0 - expandedProgress) : 1.0 + var didApplyManualIconCenter = false + + let centeredIconCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX + let iconCenterX: CGFloat + if isPrimary && expandedProgress > 0.0 { + let expandedIconCenterX: CGFloat + if isLeft { + expandedIconCenterX = backgroundFrame.maxX - metrics.expandedIconInset + } else { + expandedIconCenterX = backgroundFrame.minX + metrics.expandedIconInset + } + iconCenterX = centeredIconCenterX + (expandedIconCenterX - centeredIconCenterX) * expandedProgress + } else { + iconCenterX = centeredIconCenterX + } + let iconCenterY = backgroundFrame.midY + if let animationNode = self.animationNode { - let imageSize = CGSize(width: animationNode.size.width * self.animationScale, height: animationNode.size.height * self.animationScale) - let iconOffset: CGFloat = -2.0 + self.animationNodeOffset - let titleIconSpacing: CGFloat = 11.0 - let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - imageSize.width + sideInset) / 2.0), y: contentRect.midY - imageSize.height / 2.0 + iconOffset), size: imageSize) - if animateAdditive { - let iconOffsetX = animateIconMovement ? animationNode.frame.minX - iconFrame.minX : deltaX - animationNode.frame = iconFrame - transition.animatePositionAdditive(node: animationNode, offset: CGPoint(x: iconOffsetX, y: 0.0)) + var imageSize = CGSize(width: animationNode.size.width * self.animationScale, height: animationNode.size.height * self.animationScale) + let imageMaxSide = max(imageSize.width, imageSize.height) + if imageMaxSide > metrics.iconMaxSide { + let imageScale = metrics.iconMaxSide / imageMaxSide * 1.4 + imageSize = CGSize(width: floorToScreenPixels(imageSize.width * imageScale), height: floorToScreenPixels(imageSize.height * imageScale)) + } + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - imageSize.width / 2.0), y: floorToScreenPixels(iconCenterY - imageSize.height / 2.0) + 6.0 + self.animationNodeOffset), size: imageSize) + + if isPrimary { + didApplyManualIconCenter = true + transition.updateBounds(node: animationNode, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + let targetCenter = frameCenter(iconFrame) + if didApplyLayout && wasExpanded != isExpanded && revealProgress >= CGFloat.ulpOfOne { + self.updateManualIconCenter(iconNode: animationNode, targetCenter: targetCenter, forceImmediate: false) + } else if self.manuallyAnimatedIconNode === animationNode && self.iconAnimationLink != nil && revealProgress >= CGFloat.ulpOfOne { + self.updateManualIconCenter(iconNode: animationNode, targetCenter: targetCenter, forceImmediate: false) + } else { + self.stopManualIconAnimation() + transition.updatePosition(node: animationNode, position: targetCenter) + } } else { transition.updateFrame(node: animationNode, frame: iconFrame) } - - let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.midY + titleIconSpacing), size: titleSize) - if animateAdditive { - let titleOffsetX = animateIconMovement ? self.titleNode.frame.minX - titleFrame.minX : deltaX - self.titleNode.frame = titleFrame - transition.animatePositionAdditive(node: self.titleNode, offset: CGPoint(x: titleOffsetX, y: 0.0)) - } else { - transition.updateFrame(node: self.titleNode, frame: titleFrame) - } - if self.enableAnimations { - if (abs(revealFactor) >= 0.4) { + if revealProgress >= 0.4 { animationNode.play() - } else if abs(revealFactor) < CGFloat.ulpOfOne && !transition.isAnimated { + } else if revealProgress < CGFloat.ulpOfOne && !transition.isAnimated { animationNode.reset() } } } else if let iconNode = self.iconNode, let imageSize = iconNode.image?.size { - let iconOffset: CGFloat = -9.0 - let titleIconSpacing: CGFloat = 11.0 - let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - imageSize.width + sideInset) / 2.0), y: contentRect.midY - imageSize.height / 2.0 + iconOffset), size: imageSize) - if animateAdditive { - let iconOffsetX = animateIconMovement ? iconNode.frame.minX - iconFrame.minX : deltaX - iconNode.frame = iconFrame - transition.animatePositionAdditive(node: iconNode, offset: CGPoint(x: iconOffsetX, y: 0.0)) + var fittedSize = imageSize + let imageMaxSide = max(fittedSize.width, fittedSize.height) + if imageMaxSide > metrics.iconMaxSide { + let imageScale = metrics.iconMaxSide / imageMaxSide + fittedSize = CGSize(width: floorToScreenPixels(fittedSize.width * imageScale), height: floorToScreenPixels(fittedSize.height * imageScale)) + } + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - fittedSize.width / 2.0), y: floorToScreenPixels(iconCenterY - fittedSize.height / 2.0)), size: fittedSize) + if isPrimary { + didApplyManualIconCenter = true + transition.updateBounds(node: iconNode, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + let targetCenter = frameCenter(iconFrame) + if didApplyLayout && wasExpanded != isExpanded && revealProgress >= CGFloat.ulpOfOne { + self.updateManualIconCenter(iconNode: iconNode, targetCenter: targetCenter, forceImmediate: false) + } else if self.manuallyAnimatedIconNode === iconNode && self.iconAnimationLink != nil && revealProgress >= CGFloat.ulpOfOne { + self.updateManualIconCenter(iconNode: iconNode, targetCenter: targetCenter, forceImmediate: false) + } else { + self.stopManualIconAnimation() + transition.updatePosition(node: iconNode, position: targetCenter) + } } else { transition.updateFrame(node: iconNode, frame: iconFrame) } - - let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.midY + titleIconSpacing), size: titleSize) - if animateAdditive { - let titleOffsetX = animateIconMovement ? self.titleNode.frame.minX - titleFrame.minX : deltaX - self.titleNode.frame = titleFrame - transition.animatePositionAdditive(node: self.titleNode, offset: CGPoint(x: titleOffsetX, y: 0.0)) - } else { - transition.updateFrame(node: self.titleNode, frame: titleFrame) - } + } + + if !didApplyManualIconCenter { + self.stopManualIconAnimation() + } + transition.updateAlpha(node: self.titleNode, alpha: titleAlpha) + + let titleFrame: CGRect + if self.displaysTitleInsidePill { + titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - titleSize.width / 2.0), y: floorToScreenPixels(backgroundFrame.midY - titleSize.height / 2.0)), size: titleSize) } else { - let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((baseSize.width - titleSize.width + sideInset) / 2.0), y: contentRect.minY + floor((baseSize.height - titleSize.height) / 2.0)), size: titleSize) - if animateAdditive { - let titleOffsetX = animateIconMovement ? self.titleNode.frame.minX - titleFrame.minX : deltaX - self.titleNode.frame = titleFrame - transition.animatePositionAdditive(node: self.titleNode, offset: CGPoint(x: titleOffsetX, y: 0.0)) - } else { - transition.updateFrame(node: self.titleNode, frame: titleFrame) - } + let titleCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX + titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(titleCenterX - titleSize.width / 2.0), y: shapeFrame.maxY + optionTitleSpacing), size: titleSize) } + transition.updateFrame(node: self.titleNode, frame: titleFrame) } - + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - let titleSize = self.titleNode.measure(constrainedSize) - var maxWidth = titleSize.width - if let iconNode = self.iconNode, let image = iconNode.image { - maxWidth = max(image.size.width, maxWidth) - } - return CGSize(width: max(74.0, maxWidth + 20.0), height: constrainedSize.height) + let metrics = ItemListRevealOptionLayoutMetrics.metrics(for: constrainedSize.height, hasVisualIcons: !self.displaysTitleInsidePill) + let _ = self.titleNode.measure(CGSize(width: metrics.titleWidth, height: CGFloat.greatestFiniteMagnitude)) + return CGSize(width: metrics.slotWidth, height: constrainedSize.height) } } public final class ItemListRevealOptionsNode: ASDisplayNode { private let optionSelected: (ItemListRevealOption) -> Void private let tapticAction: () -> Void - + private let clippingContainerNode: ASDisplayNode + private let optionsContainerNode: ASDisplayNode + private var options: [ItemListRevealOption] = [] private var isLeft: Bool = false - + private var optionNodes: [ItemListRevealOptionNode] = [] private var revealOffset: CGFloat = 0.0 private var sideInset: CGFloat = 0.0 - + public init(optionSelected: @escaping (ItemListRevealOption) -> Void, tapticAction: @escaping () -> Void) { self.optionSelected = optionSelected self.tapticAction = tapticAction - + self.clippingContainerNode = ASDisplayNode() + self.optionsContainerNode = ASDisplayNode() + super.init() + + self.clippingContainerNode.clipsToBounds = true + self.addSubnode(self.clippingContainerNode) + self.clippingContainerNode.addSubnode(self.optionsContainerNode) } - + override public func didLoad() { super.didLoad() - + let gestureRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) gestureRecognizer.highlight = { [weak self] location in guard let strongSelf = self, let location = location else { @@ -311,7 +524,7 @@ public final class ItemListRevealOptionsNode: ASDisplayNode { } self.view.addGestureRecognizer(gestureRecognizer) } - + public func setOptions(_ options: [ItemListRevealOption], isLeft: Bool, enableAnimations: Bool) { if self.options != options || self.isLeft != isLeft { self.options = options @@ -320,124 +533,132 @@ public final class ItemListRevealOptionsNode: ASDisplayNode { node.removeFromSupernode() } self.optionNodes = options.map { option in - return ItemListRevealOptionNode(title: option.title, icon: option.icon, color: option.color, textColor: option.textColor, enableAnimations: enableAnimations) + return ItemListRevealOptionNode(title: option.title, icon: option.icon, color: option.color, iconColor: option.iconColor, textColor: option.textColor, enableAnimations: enableAnimations) } if isLeft { for node in self.optionNodes.reversed() { - self.addSubnode(node) + self.optionsContainerNode.addSubnode(node) } } else { for node in self.optionNodes { - self.addSubnode(node) + self.optionsContainerNode.addSubnode(node) } } self.invalidateCalculatedLayout() } } - + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - var maxWidth: CGFloat = 0.0 + let metrics = ItemListRevealOptionLayoutMetrics.metrics(for: constrainedSize.height, hasVisualIcons: self.options.contains(where: { $0.icon.hasVisualIcon })) for node in self.optionNodes { - let nodeSize = node.measure(constrainedSize) - maxWidth = max(nodeSize.width, maxWidth) + let _ = node.measure(constrainedSize) } - return CGSize(width: maxWidth * CGFloat(self.optionNodes.count), height: constrainedSize.height) + return CGSize(width: metrics.revealWidth(count: self.optionNodes.count), height: constrainedSize.height) } - + public func updateRevealOffset(offset: CGFloat, sideInset: CGFloat, transition: ContainedViewLayoutTransition) { self.revealOffset = offset self.sideInset = sideInset self.updateNodesLayout(transition: transition) } - + private func updateNodesLayout(transition: ContainedViewLayoutTransition) { let size = self.bounds.size if size.width.isLessThanOrEqualTo(0.0) || self.optionNodes.isEmpty { return } - let basicNodeWidth = floor((size.width - abs(self.sideInset)) / CGFloat(self.optionNodes.count)) - let lastNodeWidth = size.width - basicNodeWidth * CGFloat(self.optionNodes.count - 1) - let revealFactor = self.revealOffset / size.width - let boundaryRevealFactor: CGFloat - if self.optionNodes.count > 2 { - boundaryRevealFactor = 1.0 + 16.0 / size.width - } else { - boundaryRevealFactor = 1.0 + basicNodeWidth / size.width - } - let startingOffset: CGFloat + let metrics = ItemListRevealOptionLayoutMetrics.metrics(for: size.height, hasVisualIcons: self.options.contains(where: { $0.icon.hasVisualIcon })) + let revealedDistance = abs(self.revealOffset) + let boundedRevealedDistance = min(revealedDistance, size.width) + let overswipeDistance = max(0.0, revealedDistance - size.width) + let overswipeProgress = clampToUnitInterval(overswipeDistance / optionExpandedTransitionDistance) + let expandedActivationDistance = metrics.shapeSize.width * (optionExpandedActivationWidthFactor - 1.0) + let primaryIndex = self.isLeft ? 0 : self.optionNodes.count - 1 + let stride = metrics.shapeSize.width + optionSpacing + + let clippingFrameX: CGFloat if self.isLeft { - startingOffset = size.width + max(0.0, abs(revealFactor) - 1.0) * size.width + clippingFrameX = max(0.0, size.width - revealedDistance) } else { - startingOffset = 0.0 + clippingFrameX = 0.0 } - + let clippingFrame = CGRect(origin: CGPoint(x: clippingFrameX, y: 0.0), size: CGSize(width: revealedDistance, height: size.height)) + transition.updateFrame(node: self.clippingContainerNode, frame: clippingFrame) + transition.updateFrame(node: self.optionsContainerNode, frame: CGRect(origin: CGPoint(x: -clippingFrameX, y: 0.0), size: CGSize(width: max(size.width, revealedDistance), height: size.height))) + let animated = transition.isAnimated var completionCount = self.optionNodes.count let intermediateCompletion = { - if completionCount == 0 && animated && abs(revealFactor) < CGFloat.ulpOfOne { + if completionCount == 0 && animated && revealedDistance < CGFloat.ulpOfOne { for node in self.optionNodes { node.resetAnimation() } } } - + var i = self.isLeft ? (self.optionNodes.count - 1) : 0 while i >= 0 && i < self.optionNodes.count { let node = self.optionNodes[i] - let nodeWidth = i == (self.optionNodes.count - 1) ? lastNodeWidth : basicNodeWidth - var nodeTransition = transition - var isExpanded = false - if (self.isLeft && i == 0) || (!self.isLeft && i == self.optionNodes.count - 1) { - if abs(revealFactor) > boundaryRevealFactor { - isExpanded = true + let isPrimary = i == primaryIndex + let isStretched = isPrimary && overswipeDistance > CGFloat.ulpOfOne + let isExpanded = isPrimary && overswipeDistance > expandedActivationDistance + let expandedProgress: CGFloat = isExpanded ? 1.0 : 0.0 + if node.hasAppliedLayout && node.isExpanded != isExpanded && !transition.isAnimated { + self.tapticAction() + } + + let baseCircleFrame: CGRect + let nodeFrame: CGRect + let revealProgress: CGFloat + + if self.isLeft { + let baseCircleLeft = size.width - boundedRevealedDistance + self.sideInset + optionEdgeInset + CGFloat(i) * stride + baseCircleFrame = CGRect(origin: CGPoint(x: baseCircleLeft, y: 0.0), size: metrics.shapeSize) + let distanceFromShutterEdge = size.width - baseCircleFrame.maxX + revealProgress = clampToUnitInterval((distanceFromShutterEdge + optionRevealStartOverlap) / (optionRevealStartOverlap + optionRevealEndDistance)) + + if isStretched { + let primaryLeft = size.width - boundedRevealedDistance + self.sideInset + optionEdgeInset + let primaryRight: CGFloat + if self.optionNodes.count > 1 { + let neighborLeft = primaryLeft + stride + overswipeDistance + primaryRight = max(primaryLeft + metrics.shapeSize.width, neighborLeft - optionSpacing) + } else { + primaryRight = primaryLeft + metrics.shapeSize.width + overswipeDistance + } + nodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(primaryLeft), y: 0.0), size: CGSize(width: max(metrics.shapeSize.width, primaryRight - primaryLeft), height: size.height)) + } else { + let circleLeft = baseCircleLeft + (isPrimary ? 0.0 : overswipeDistance) + nodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(circleLeft - metrics.slotShapeInset), y: 0.0), size: CGSize(width: metrics.slotWidth, height: size.height)) } - } - if let _ = node.alignment, node.isExpanded != isExpanded { - nodeTransition = transition.isAnimated ? transition : .animated(duration: 0.2, curve: .easeInOut) - if !transition.isAnimated { - self.tapticAction() - } - } - - var sideInset: CGFloat = 0.0 - if i == self.optionNodes.count - 1 { - sideInset = self.sideInset - } - - let extendedWidth: CGFloat - let nodeLeftOffset: CGFloat - if isExpanded { - nodeLeftOffset = 0.0 - extendedWidth = size.width * max(1.0, abs(revealFactor)) - } else if self.isLeft { - let offset = basicNodeWidth * CGFloat(self.optionNodes.count - 1 - i) - extendedWidth = (size.width - offset) * max(1.0, abs(revealFactor)) - nodeLeftOffset = startingOffset - extendedWidth - floorToScreenPixels(offset * abs(revealFactor)) } else { - let offset = basicNodeWidth * CGFloat(i) - extendedWidth = (size.width - offset) * max(1.0, abs(revealFactor)) - nodeLeftOffset = startingOffset + floorToScreenPixels(offset * abs(revealFactor)) + let baseCircleRight = revealedDistance + self.sideInset - optionEdgeInset - CGFloat(self.optionNodes.count - 1 - i) * stride + baseCircleFrame = CGRect(origin: CGPoint(x: baseCircleRight - metrics.shapeSize.width, y: 0.0), size: metrics.shapeSize) + revealProgress = clampToUnitInterval((baseCircleFrame.minX + optionRevealStartOverlap) / (optionRevealStartOverlap + optionRevealEndDistance)) + + if isStretched { + let primaryRight = revealedDistance + self.sideInset - optionEdgeInset + let primaryLeft: CGFloat + if self.optionNodes.count > 1 { + let neighborRight = primaryRight - stride - overswipeDistance + primaryLeft = min(primaryRight - metrics.shapeSize.width, neighborRight + optionSpacing) + } else { + primaryLeft = primaryRight - metrics.shapeSize.width - overswipeDistance + } + nodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(primaryLeft), y: 0.0), size: CGSize(width: max(metrics.shapeSize.width, primaryRight - primaryLeft), height: size.height)) + } else { + let circleLeft = baseCircleFrame.minX - (isPrimary ? 0.0 : overswipeDistance) + nodeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(circleLeft - metrics.slotShapeInset), y: 0.0), size: CGSize(width: metrics.slotWidth, height: size.height)) + } } - - transition.updateFrame(node: node, frame: CGRect(origin: CGPoint(x: nodeLeftOffset, y: 0.0), size: CGSize(width: extendedWidth, height: size.height)), completion: { _ in + + transition.updateFrame(node: node, frame: nodeFrame, completion: { _ in completionCount -= 1 intermediateCompletion() - }) - - var nodeAlignment: ItemListRevealOptionAlignment - if (self.optionNodes.count > 1) { - nodeAlignment = self.isLeft ? .right : .left - } else { - if (self.isLeft) { - nodeAlignment = isExpanded ? .right : .left - } else { - nodeAlignment = isExpanded ? .left : .right - } - } - let animateIconMovement = self.optionNodes.count == 1 - node.updateLayout(isFirst: (self.isLeft && i == 0) || (!self.isLeft && i == self.optionNodes.count - 1), isLeft: self.isLeft, baseSize: CGSize(width: nodeWidth, height: size.height), alignment: nodeAlignment, isExpanded: isExpanded, extendedWidth: extendedWidth, sideInset: sideInset, transition: nodeTransition, additive: !transition.isAnimated, revealFactor: revealFactor, animateIconMovement: animateIconMovement) - + + node.updateLayout(isLeft: self.isLeft, isPrimary: isPrimary, metrics: metrics, revealProgress: revealProgress, overswipeProgress: overswipeProgress, expandedProgress: expandedProgress, isStretched: isStretched, isExpanded: isExpanded, transition: transition) + if self.isLeft { i -= 1 } else { @@ -445,12 +666,12 @@ public final class ItemListRevealOptionsNode: ASDisplayNode { } } } - + @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { if case .ended = recognizer.state, let gesture = recognizer.lastRecognizedGestureAndLocation?.0, case .tap = gesture { let location = recognizer.location(in: self.view) var selectedOption: Int? - + var i = self.isLeft ? 0 : (self.optionNodes.count - 1) while i >= 0 && i < self.optionNodes.count { self.optionNodes[i].setHighlighted(false) @@ -469,7 +690,7 @@ public final class ItemListRevealOptionsNode: ASDisplayNode { } } } - + public func isDisplayingExtendedAction() -> Bool { return self.optionNodes.contains(where: { $0.isExpanded }) } diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift index 5017682d7b..d59871b99b 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift @@ -124,6 +124,7 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { private let subtitleNode: TextNode private var item: ItemListCheckboxItem? + private var isHighlighted = false override public var controlsContainer: ASDisplayNode { return self.contentParentNode @@ -336,27 +337,31 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false + let topStripeIsHidden: Bool switch neighbors.top { case .sameSection(false): - strongSelf.topStripeNode.isHidden = true + topStripeIsHidden = true default: hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners + topStripeIsHidden = hasCorners } let bottomStripeInset: CGFloat + let bottomStripeIsHidden: Bool if item.zeroSeparatorInsets { bottomStripeInset = 0.0 + bottomStripeIsHidden = false } else { switch neighbors.bottom { case .sameSection(false): bottomStripeInset = leftInset - strongSelf.bottomStripeNode.isHidden = false + bottomStripeIsHidden = false default: bottomStripeInset = 0.0 hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners + bottomStripeIsHidden = hasCorners } } + strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil @@ -384,12 +389,12 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { strongSelf.imageNode.image = nil } - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) + strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))) strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) if item.deleteAction != nil { - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)])) } else { strongSelf.setRevealOptions((left: [], right: [])) } @@ -401,39 +406,8 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted && (self.item?.enabled ?? false) { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } + self.isHighlighted = highlighted && (self.item?.enabled ?? false) + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.4, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) } override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { @@ -446,10 +420,16 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { super.updateRevealOffset(offset: offset, transition: transition) - + transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: CGPoint(x: offset, y: self.contentContainerNode.frame.minY), size: self.contentContainerNode.bounds.size)) } + override public func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) + } + override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { self.setRevealOptionsOpened(false, animated: true) self.revealOptionsInteractivelyClosed() diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index fc62243b86..ab096a8865 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -74,7 +74,33 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemCompone public let tag: ItemListItemTag? public let shimmeringIndex: Int? - public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, titleBadge: String? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) { + public init( + presentationData: ItemListPresentationData, + systemStyle: ItemListSystemStyle = .legacy, + icon: UIImage? = nil, + context: AccountContext? = nil, + iconPeer: EnginePeer? = nil, + title: String, + attributedTitle: NSAttributedString? = nil, + enabled: Bool = true, + titleColor: ItemListDisclosureItemTitleColor = .primary, + titleFont: ItemListDisclosureItemTitleFont = .regular, + titleIcon: UIImage? = nil, + titleBadge: String? = nil, + label: String, + attributedLabel: NSAttributedString? = nil, + labelStyle: ItemListDisclosureLabelStyle = .text, + additionalDetailLabel: String? = nil, + additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, + sectionId: ItemListSectionId, + style: ItemListStyle, + disclosureStyle: ItemListDisclosureStyle = .arrow, + noInsets: Bool = false, + action: (() -> Void)?, + clearHighlightAutomatically: Bool = true, + tag: ItemListItemTag? = nil, + shimmeringIndex: Int? = nil + ) { self.presentationData = presentationData self.systemStyle = systemStyle self.icon = icon @@ -709,7 +735,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { centralContentHeight += additionalDetailLabelInfo.0.size.height } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((height - centralContentHeight) / 2.0) + 1.0), size: titleLayout.size) strongSelf.titleNode.textNode.frame = titleFrame if let updateBadgeImage = updatedLabelBadgeImage { @@ -739,7 +765,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { case .detailText, .multilineDetailText: labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) default: - labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size) + labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floorToScreenPixels((height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size) } strongSelf.labelNode.frame = labelFrame diff --git a/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift b/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift index df2372c151..955a0d6201 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListEditableItem.swift @@ -67,17 +67,173 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD private var enableAnimations: Bool = true private var initialRevealOffset: CGFloat = 0.0 + private var hasActiveRevealGestureOffset: Bool = false public private(set) var revealOffset: CGFloat = 0.0 - + public private(set) var isRevealOptionsActive: Bool = false + public private(set) var isNextRevealOptionsActive: Bool = false + private weak var revealOptionsActivePreviousItemNode: ItemListRevealOptionsItemNode? + + private weak var revealOptionsTopSeparatorNode: ASDisplayNode? + private weak var revealOptionsBottomSeparatorNode: ASDisplayNode? + private var revealOptionsTopSeparatorBaseIsHidden: Bool = false + private var revealOptionsBottomSeparatorBaseIsHidden: Bool = false + private var revealOptionsTopSeparatorHiddenByPreviousRevealOptions: Bool = false + private var revealOptionsBottomSeparatorHiddenByNextRevealOptions: Bool = false + + private weak var revealOptionsHighlightedBackgroundNode: ASDisplayNode? + private var revealOptionsHighlightedBackgroundBaseFrame: CGRect? + private var revealOptionsHighlightedBackgroundTracksRevealOffset: Bool = false + private var revealOptionsHighlightedBackgroundActiveCornerRadius: CGFloat = 0.0 + private var recognizer: ItemListRevealOptionsGestureRecognizer? private var tapRecognizer: UITapGestureRecognizer? private var hapticFeedback: HapticFeedback? private var allowAnyDirection = false - + public var isDisplayingRevealedOptions: Bool { return !self.revealOffset.isZero } + + private func updateRevealOptionsActive(_ isActive: Bool, transition: ContainedViewLayoutTransition) { + if self.isRevealOptionsActive != isActive { + self.isRevealOptionsActive = isActive + self.updatePreviousRevealOptionsItemNode(isActive: isActive, transition: transition) + self.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + } + } + + private func updateNextRevealOptionsActive(_ isActive: Bool, transition: ContainedViewLayoutTransition) { + if self.isNextRevealOptionsActive != isActive { + self.isNextRevealOptionsActive = isActive + self.nextRevealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + } + } + + private func previousRevealOptionsItemNode() -> ItemListRevealOptionsItemNode? { + guard let subnodes = self.supernode?.subnodes else { + return nil + } + + if let index = self.index { + var candidate: ItemListRevealOptionsItemNode? + for subnode in subnodes { + guard let itemNode = subnode as? ItemListRevealOptionsItemNode, itemNode !== self, let itemIndex = itemNode.index, itemIndex < index else { + continue + } + if let currentCandidate = candidate, let candidateIndex = currentCandidate.index { + if itemIndex > candidateIndex { + candidate = itemNode + } + } else { + candidate = itemNode + } + } + if let candidate = candidate { + return candidate + } + } + + var candidate: ItemListRevealOptionsItemNode? + for subnode in subnodes { + guard let itemNode = subnode as? ItemListRevealOptionsItemNode, itemNode !== self else { + continue + } + if itemNode.frame.maxY <= self.frame.minY + UIScreenPixel { + if let currentCandidate = candidate { + if itemNode.frame.maxY > currentCandidate.frame.maxY { + candidate = itemNode + } + } else { + candidate = itemNode + } + } + } + return candidate + } + + private func updatePreviousRevealOptionsItemNode(isActive: Bool, transition: ContainedViewLayoutTransition) { + let previousItemNode = isActive ? self.previousRevealOptionsItemNode() : nil + if self.revealOptionsActivePreviousItemNode !== previousItemNode { + self.revealOptionsActivePreviousItemNode?.updateNextRevealOptionsActive(false, transition: transition) + self.revealOptionsActivePreviousItemNode = previousItemNode + } + previousItemNode?.updateNextRevealOptionsActive(isActive, transition: transition) + if !isActive { + self.revealOptionsActivePreviousItemNode = nil + } + } + + private func updateRevealOptionsSeparatorVisibility() { + self.revealOptionsTopSeparatorNode?.isHidden = self.revealOptionsTopSeparatorBaseIsHidden || self.revealOptionsTopSeparatorHiddenByPreviousRevealOptions || self.isRevealOptionsActive + self.revealOptionsBottomSeparatorNode?.isHidden = self.revealOptionsBottomSeparatorBaseIsHidden || self.revealOptionsBottomSeparatorHiddenByNextRevealOptions || self.isRevealOptionsActive || self.isNextRevealOptionsActive + } + + private func updateRevealOptionsHighlightedBackgroundGeometry(transition: ContainedViewLayoutTransition) { + guard let highlightedBackgroundNode = self.revealOptionsHighlightedBackgroundNode, let baseFrame = self.revealOptionsHighlightedBackgroundBaseFrame else { + return + } + + let frame: CGRect + if self.revealOptionsHighlightedBackgroundTracksRevealOffset && self.isRevealOptionsActive { + frame = baseFrame.offsetBy(dx: self.revealOffset, dy: 0.0) + } else { + frame = baseFrame + } + transition.updateFrame(node: highlightedBackgroundNode, frame: frame) + transition.updateCornerRadius(node: highlightedBackgroundNode, cornerRadius: self.isRevealOptionsActive ? self.revealOptionsHighlightedBackgroundActiveCornerRadius : 0.0) + } + + public func updateRevealOptionsSeparatorNodes(top: ASDisplayNode?, bottom: ASDisplayNode?, topIsHidden: Bool, bottomIsHidden: Bool, topHiddenByPreviousRevealOptions: Bool = false, bottomHiddenByNextRevealOptions: Bool = false) { + self.revealOptionsTopSeparatorNode = top + self.revealOptionsBottomSeparatorNode = bottom + self.revealOptionsTopSeparatorBaseIsHidden = topIsHidden + self.revealOptionsBottomSeparatorBaseIsHidden = bottomIsHidden + self.revealOptionsTopSeparatorHiddenByPreviousRevealOptions = topHiddenByPreviousRevealOptions + self.revealOptionsBottomSeparatorHiddenByNextRevealOptions = bottomHiddenByNextRevealOptions + self.updateRevealOptionsSeparatorVisibility() + } + + public func updateRevealOptionsHighlightedBackgroundFrame(_ highlightedBackgroundNode: ASDisplayNode, frame: CGRect, tracksRevealOffset: Bool = true, activeCornerRadius: CGFloat = 26.0, transition: ContainedViewLayoutTransition = .immediate) { + self.revealOptionsHighlightedBackgroundNode = highlightedBackgroundNode + self.revealOptionsHighlightedBackgroundBaseFrame = frame + self.revealOptionsHighlightedBackgroundTracksRevealOffset = tracksRevealOffset + self.revealOptionsHighlightedBackgroundActiveCornerRadius = activeCornerRadius + self.updateRevealOptionsHighlightedBackgroundGeometry(transition: transition) + } + + public func updateRevealOptionsHighlightedBackgroundNode(_ highlightedBackgroundNode: ASDisplayNode, isHighlighted: Bool, transition: ContainedViewLayoutTransition, aboveNodes: [ASDisplayNode?]) { + self.revealOptionsHighlightedBackgroundNode = highlightedBackgroundNode + if isHighlighted { + highlightedBackgroundNode.alpha = 1.0 + if highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + for node in aboveNodes { + if let node = node, node.supernode != nil { + anchorNode = node + break + } + } + if let anchorNode = anchorNode { + self.insertSubnode(highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(highlightedBackgroundNode) + } + } + } else if highlightedBackgroundNode.supernode != nil { + if transition.isAnimated { + highlightedBackgroundNode.layer.animateAlpha(from: highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak highlightedBackgroundNode] completed in + if completed { + highlightedBackgroundNode?.removeFromSupernode() + } + }) + highlightedBackgroundNode.alpha = 0.0 + } else { + highlightedBackgroundNode.removeFromSupernode() + } + } + self.updateRevealOptionsHighlightedBackgroundGeometry(transition: transition) + } override open var canBeSelected: Bool { return !self.isDisplayingRevealedOptions @@ -86,6 +242,10 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD override public init(layerBacked: Bool, rotated: Bool, seeThrough: Bool) { super.init(layerBacked: layerBacked, rotated: rotated, seeThrough: seeThrough) } + + deinit { + self.revealOptionsActivePreviousItemNode?.updateNextRevealOptionsActive(false, transition: .immediate) + } open var controlsContainer: ASDisplayNode { return self @@ -185,25 +345,27 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD } open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { - /*if gestureRecognizer === self.recognizer && otherGestureRecognizer is InteractiveTransitionGestureRecognizer { - return true - }*/ return false } @objc private func revealTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) + self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0))) self.revealOptionsInteractivelyClosed() } } + private func revealSpringCurve(initialVelocity: CGFloat = 0.0) -> ContainedViewLayoutTransitionCurve { + return .customSpring(mass: 2.0, stiffness: 200.0, damping: 100.0, initialVelocity: initialVelocity) + } + @objc private func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) { guard let (size, _, _) = self.validLayout else { return } switch recognizer.state { case .began: + self.hasActiveRevealGestureOffset = !self.revealOffset.isZero if let leftRevealNode = self.leftRevealNode { let revealSize = leftRevealNode.bounds.size let location = recognizer.location(in: self.view) @@ -235,16 +397,26 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD if self.leftRevealNode == nil && CGFloat(0.0).isLess(than: translation.x) { self.setupAndAddLeftRevealNode() self.revealOptionsInteractivelyOpened() + self.hasActiveRevealGestureOffset = true } else if self.rightRevealNode == nil && translation.x.isLess(than: 0.0) { self.setupAndAddRightRevealNode() self.revealOptionsInteractivelyOpened() + self.hasActiveRevealGestureOffset = true + } + if !translation.x.isZero { + self.hasActiveRevealGestureOffset = true } self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate) - if self.leftRevealNode == nil && self.rightRevealNode == nil { + if self.leftRevealNode == nil && self.rightRevealNode == nil && !self.hasActiveRevealGestureOffset { self.revealOptionsInteractivelyClosed() } case .ended, .cancelled: + let hasActiveRevealGestureOffset = self.hasActiveRevealGestureOffset + self.hasActiveRevealGestureOffset = false guard let recognizer = self.recognizer else { + if hasActiveRevealGestureOffset { + self.revealOptionsInteractivelyClosed() + } break } @@ -273,7 +445,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD reveal = false selectedOption = self.revealOptions.left.first } else { - self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring)) + self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0))) } if let selectedOption = selectedOption { @@ -308,7 +480,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD reveal = false selectedOption = self.revealOptions.right.last } else { - self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring)) + self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0))) } if let selectedOption = selectedOption { @@ -318,6 +490,8 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD self.revealOptionsInteractivelyClosed() } } + } else if hasActiveRevealGestureOffset { + self.revealOptionsInteractivelyClosed() } default: break @@ -370,6 +544,9 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { self.validLayout = (size, leftInset, rightInset) + if self.isRevealOptionsActive { + self.updatePreviousRevealOptionsItemNode(isActive: true, transition: .immediate) + } if let leftRevealNode = self.leftRevealNode { var revealSize = leftRevealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: size.height)) @@ -386,6 +563,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD open func updateRevealOffsetInternal(offset: CGFloat, transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { self.revealOffset = offset + self.updateRevealOptionsActive(!offset.isZero, transition: transition) guard let (size, leftInset, rightInset) = self.validLayout else { return } @@ -455,11 +633,21 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD } self.updateRevealOffset(offset: offset, transition: transition) + self.updateRevealOptionsHighlightedBackgroundGeometry(transition: transition) } open func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { } + + open func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + self.updateRevealOptionsSeparatorVisibility() + self.updateRevealOptionsHighlightedBackgroundGeometry(transition: transition) + } + + open func nextRevealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + self.updateRevealOptionsSeparatorVisibility() + } open func revealOptionsInteractivelyOpened() { @@ -476,7 +664,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD } let transition: ContainedViewLayoutTransition if animated { - transition = .animated(duration: 0.3, curve: .spring) + transition = .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)) } else { transition = .immediate } @@ -498,7 +686,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD open func animateRevealOptionsFill(completion: (() -> Void)? = nil) { if let validLayout = self.validLayout { self.layer.allowsGroupOpacity = true - self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .animated(duration: 0.3, curve: .spring), completion: { + self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)), completion: { self.layer.allowsGroupOpacity = false completion?() }) diff --git a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift index b21392f866..c238ddb3a3 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift @@ -174,12 +174,15 @@ public class InfoItemNode: ListViewItemNode { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false + self.labelNode.displaysAsynchronously = false self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false self.textNode = TextNode() self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false self.activateArea = AccessibilityAreaNode() self.activateArea.accessibilityTraits = .staticText diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index bb6552e96f..e9f93e82c2 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -171,6 +171,10 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { public var tag: ItemListItemTag? { return self.item?.tag } + + public var contextSourceView: UIView { + return self.textNode?.view ?? self.switchNode.view + } public init(type: ItemListSwitchItemNodeType) { self.backgroundNode = ASDisplayNode() @@ -251,6 +255,20 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { }) }) } + + public func updateHasContextMenu(hasContextMenu: Bool) { + let transition: ContainedViewLayoutTransition + if hasContextMenu { + transition = .immediate + } else { + transition = .animated(duration: 0.3, curve: .easeInOut) + } + if let textNode = self.textNode { + transition.updateAlpha(node: textNode, alpha: hasContextMenu ? 0.5 : 1.0) + } else { + transition.updateAlpha(node: self.switchNode, alpha: hasContextMenu ? 0.5 : 1.0) + } + } func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -488,7 +506,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight))) } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset + 1.0), size: titleLayout.size) transition.updatePosition(node: strongSelf.titleNode, position: titleFrame.origin) strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index 3eed8b32db..63c7349d2d 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -141,14 +141,19 @@ public final class LegacyJoinLinkPreviewController: ViewController { private func join() { self.disposable.set((self.context.engine.peers.joinChatInteractively(with: self.link) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { - if strongSelf.isRequest { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .inviteRequestSent(title: strongSelf.presentationData.strings.MemberRequests_RequestToJoinSent, text: strongSelf.isGroup ? strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } else { - if case let .joined(peer) = result, let peer { - strongSelf.navigateToPeer(peer, nil) + switch result { + case let .joined(peer): + if strongSelf.isRequest { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .inviteRequestSent(title: strongSelf.presentationData.strings.MemberRequests_RequestToJoinSent, text: strongSelf.isGroup ? strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : strongSelf.presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } else { + if let peer { + strongSelf.navigateToPeer(peer, nil) + } } + strongSelf.dismiss() + case let .webView(webView): + strongSelf.context.sharedContext.openJoinChatWebView(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: nil, webView: webView) } - strongSelf.dismiss() } }, error: { [weak self] error in if let strongSelf = self { diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift index 7516769aef..302c8ceae6 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift @@ -81,6 +81,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, ASScrollVi self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index a0ce070a00..2a3f2b576b 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -133,6 +133,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer self.descriptionNode.textAlignment = .center self.peersScrollNode = ASScrollNode() self.peersScrollNode.view.showsHorizontalScrollIndicator = false + self.peersScrollNode.view.scrollsToTop = false self.actionButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 11.0) diff --git a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift index 95fd3d3982..53382b7d12 100644 --- a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift +++ b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift @@ -76,6 +76,7 @@ final class LanguageLinkPreviewControllerNode: ViewControllerTracingNode, ASScro self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h index 847e2b5c7c..d0d054545a 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerGalleryInterfaceView.h @@ -14,7 +14,7 @@ @property (nonatomic, copy) void (^captionSet)(id, NSAttributedString *); @property (nonatomic, copy) void (^donePressed)(id); -@property (nonatomic, copy) void (^doneLongPressed)(id); +@property (nonatomic, copy) void (^doneLongPressed)(id, UIView *); @property (nonatomic, copy) void (^photoStripItemSelected)(NSInteger index); diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index 4afdca957e..1e8c90aa47 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -1,6 +1,7 @@ #import #import #import +#import @class TGPaintingData; @class TGStickerMaskDescription; @@ -142,6 +143,8 @@ @property (nonatomic, copy) id _Nullable(^ _Nullable captionPanelView)(void); @property (nonatomic, copy) id _Nullable(^ _Nullable livePhotoButton)(void); +@property (nonatomic, copy) UIView *_Nullable(^ _Nullable photoToolbarView)(TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, bool hasSendStarsButton); +@property (nonatomic, copy) bool (^ _Nullable presentMediaPickerSendActionMenu)(UIView * _Nonnull sourceView, bool canSendSilently, bool canSendWhenOnline, bool canSchedule, bool reminder, bool hasTimer, void (^ _Nonnull sendSilently)(void), void (^ _Nonnull sendWhenOnline)(void), void (^ _Nonnull schedule)(void), void (^ _Nonnull sendWithTimer)(void)); @property (nonatomic, copy) void (^ _Nullable editCover)(CGSize dimensions, void(^_Nonnull completion)(UIImage * _Nonnull)); diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h index 4860d82e10..90e8982d23 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarView.h @@ -1,44 +1,14 @@ #import +#import #import @protocol TGPhotoPaintStickersContext; -typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) { - TGPhotoEditorNoneTab = 0, - TGPhotoEditorCropTab = 1 << 0, - TGPhotoEditorRotateTab = 1 << 1, - TGPhotoEditorMirrorTab = 1 << 2, - TGPhotoEditorPaintTab = 1 << 3, - TGPhotoEditorEraserTab = 1 << 4, - TGPhotoEditorStickerTab = 1 << 5, - TGPhotoEditorTextTab = 1 << 6, - TGPhotoEditorToolsTab = 1 << 7, - TGPhotoEditorQualityTab = 1 << 8, - TGPhotoEditorTimerTab = 1 << 9, - TGPhotoEditorAspectRatioTab = 1 << 10, - TGPhotoEditorTintTab = 1 << 11, - TGPhotoEditorBlurTab = 1 << 12, - TGPhotoEditorCurvesTab = 1 << 13 -}; - -typedef enum -{ - TGPhotoEditorBackButtonBack, - TGPhotoEditorBackButtonCancel -} TGPhotoEditorBackButton; - -typedef enum -{ - TGPhotoEditorDoneButtonSend, - TGPhotoEditorDoneButtonCheck, - TGPhotoEditorDoneButtonDone, - TGPhotoEditorDoneButtonSchedule -} TGPhotoEditorDoneButton; - -@interface TGPhotoToolbarView : UIView +@interface TGPhotoToolbarView : UIView @property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation; +@property (nonatomic, assign) CGFloat bottomInset; @property (nonatomic, readonly) UIButton *doneButton; @@ -79,8 +49,12 @@ typedef enum - (void)setActiveTab:(TGPhotoEditorTab)tab; +- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset; +- (void)setTimerButtonValue:(NSInteger)value; + - (void)setInfoString:(NSString *)string; +- (UIView *)viewForTab:(TGPhotoEditorTab)tab; - (TGPhotoEditorButton *)buttonForTab:(TGPhotoEditorTab)tab; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarViewProtocol.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarViewProtocol.h new file mode 100644 index 0000000000..043207594e --- /dev/null +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoToolbarViewProtocol.h @@ -0,0 +1,83 @@ +#import +#import + +typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) { + TGPhotoEditorNoneTab = 0, + TGPhotoEditorCropTab = 1 << 0, + TGPhotoEditorRotateTab = 1 << 1, + TGPhotoEditorMirrorTab = 1 << 2, + TGPhotoEditorPaintTab = 1 << 3, + TGPhotoEditorEraserTab = 1 << 4, + TGPhotoEditorStickerTab = 1 << 5, + TGPhotoEditorTextTab = 1 << 6, + TGPhotoEditorToolsTab = 1 << 7, + TGPhotoEditorQualityTab = 1 << 8, + TGPhotoEditorTimerTab = 1 << 9, + TGPhotoEditorAspectRatioTab = 1 << 10, + TGPhotoEditorTintTab = 1 << 11, + TGPhotoEditorBlurTab = 1 << 12, + TGPhotoEditorCurvesTab = 1 << 13 +}; + +typedef enum +{ + TGPhotoEditorBackButtonBack, + TGPhotoEditorBackButtonCancel +} TGPhotoEditorBackButton; + +typedef enum +{ + TGPhotoEditorDoneButtonSend, + TGPhotoEditorDoneButtonCheck, + TGPhotoEditorDoneButtonDone, + TGPhotoEditorDoneButtonSchedule +} TGPhotoEditorDoneButton; + +@protocol TGPhotoToolbarViewProtocol + +@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation; +@property (nonatomic, assign) CGFloat bottomInset; + +@property (nonatomic, readonly) UIView *doneButton; + +@property (nonatomic, copy) void(^cancelPressed)(void); +@property (nonatomic, copy) void(^donePressed)(void); +@property (nonatomic, copy) void(^doneLongPressed)(id sender); +@property (nonatomic, copy) void(^tabPressed)(TGPhotoEditorTab tab); + +@property (nonatomic, readonly) CGRect cancelButtonFrame; +@property (nonatomic, readonly) CGRect doneButtonFrame; + +@property (nonatomic, assign) TGPhotoEditorBackButton backButtonType; +@property (nonatomic, assign) TGPhotoEditorDoneButton doneButtonType; + +@property (nonatomic, assign) int64_t sendPaidMessageStars; + +@property (nonatomic, readonly) TGPhotoEditorTab currentTabs; + +- (void)transitionInAnimated:(bool)animated; +- (void)transitionInAnimated:(bool)animated transparent:(bool)transparent; +- (void)transitionOutAnimated:(bool)animated; +- (void)transitionOutAnimated:(bool)animated transparent:(bool)transparent hideOnCompletion:(bool)hideOnCompletion; + +- (void)setDoneButtonEnabled:(bool)enabled animated:(bool)animated; +- (void)setEditButtonsEnabled:(bool)enabled animated:(bool)animated; +- (void)setEditButtonsHidden:(bool)hidden animated:(bool)animated; +- (void)setEditButtonsHighlighted:(TGPhotoEditorTab)buttons; +- (void)setEditButtonsDisabled:(TGPhotoEditorTab)buttons; + +- (void)setCenterButtonsHidden:(bool)hidden animated:(bool)animated; +- (void)setAllButtonsHidden:(bool)hidden animated:(bool)animated; +- (void)setCancelDoneButtonsHidden:(bool)hidden animated:(bool)animated; + +- (void)setToolbarTabs:(TGPhotoEditorTab)tabs animated:(bool)animated; +- (void)setActiveTab:(TGPhotoEditorTab)tab; + +- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset; +- (void)setTimerButtonValue:(NSInteger)value; + +- (void)setInfoString:(NSString *)string; + +- (UIView *)viewForTab:(TGPhotoEditorTab)tab; + +@end diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index ee9724b20e..10f823ceda 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -1548,7 +1548,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus __weak TGModernGalleryController *weakGalleryController = galleryController; __weak TGMediaPickerGalleryModel *weakModel = model; - model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) { + model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) { __strong TGCameraController *strongSelf = weakSelf; __strong TGMediaPickerGalleryModel *strongModel = weakModel; if (strongSelf == nil || !(strongSelf.hasSilentPosting || strongSelf.hasSchedule) || strongSelf->_shortcut) @@ -1720,6 +1720,21 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus [strongSelf _dismissTransitionForResultController:strongController]; }); }; + if (sourceView != nil && strongSelf.stickersContext.presentMediaPickerSendActionMenu != nil && strongSelf.stickersContext.presentMediaPickerSendActionMenu(sourceView, strongSelf->_hasSilentPosting, effectiveHasSchedule, effectiveHasSchedule, strongSelf->_reminder, strongSelf->_hasTimer, ^{ + if (controller.sendSilently != nil) + controller.sendSilently(); + }, ^{ + if (controller.sendWhenOnline != nil) + controller.sendWhenOnline(); + }, ^{ + if (controller.schedule != nil) + controller.schedule(); + }, ^{ + if (controller.sendWithTimer != nil) + controller.sendWithTimer(); + })) { + return; + } id windowManager = nil; windowManager = [strongSelf->_context makeOverlayWindowManager]; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index 28564775a5..84a2531334 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -42,6 +42,19 @@ #import #import +#import + +static UIView *TGMediaPickerCreatePhotoToolbarView(id context, TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, id stickersContext, bool hasSendStarsButton) +{ + if (stickersContext.photoToolbarView != nil) + { + UIView *toolbarView = stickersContext.photoToolbarView(backButton, doneButton, solidBackground, hasSendStarsButton); + if (toolbarView != nil) + return toolbarView; + } + + return [[TGPhotoToolbarView alloc] initWithContext:context backButton:backButton doneButton:doneButton solidBackground:solidBackground stickersContext:hasSendStarsButton ? stickersContext : nil]; +} static TGMediaAsset *TGMediaPickerGalleryLivePhotoAsset(id editableMediaItem) { @@ -101,8 +114,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * UIView *_wrapperView; UIView *_headerWrapperView; - TGPhotoToolbarView *_portraitToolbarView; - TGPhotoToolbarView *_landscapeToolbarView; + UIView *_portraitToolbarView; + UIView *_landscapeToolbarView; UIImageView *_arrowView; UILabel *_recipientLabel; @@ -224,8 +237,10 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * return; [strongSelf.window endEditing:true]; - if (strongSelf->_doneLongPressed != nil) - strongSelf->_doneLongPressed(strongSelf->_currentItem); + if (strongSelf->_doneLongPressed != nil) { + UIView *sourceView = [sender isKindOfClass:[UIView class]] ? (UIView *)sender : nil; + strongSelf->_doneLongPressed(strongSelf->_currentItem, sourceView); + } [[NSUserDefaults standardUserDefaults] setObject:@(3) forKey:@"TG_displayedMediaTimerTooltip_v3"]; }; @@ -471,13 +486,13 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * TGPhotoEditorDoneButton doneButton = isScheduledMessages ? TGPhotoEditorDoneButtonSchedule : TGPhotoEditorDoneButtonSend; - _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:editingContext.sendPaidMessageStars > 0 ? stickersContext : nil]; + _portraitToolbarView = TGMediaPickerCreatePhotoToolbarView(_context, TGPhotoEditorBackButtonBack, doneButton, false, stickersContext, editingContext.sendPaidMessageStars > 0); _portraitToolbarView.cancelPressed = toolbarCancelPressed; _portraitToolbarView.donePressed = toolbarDonePressed; _portraitToolbarView.doneLongPressed = toolbarDoneLongPressed; [_wrapperView addSubview:_portraitToolbarView]; - _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:nil]; + _landscapeToolbarView = TGMediaPickerCreatePhotoToolbarView(_context, TGPhotoEditorBackButtonBack, doneButton, false, stickersContext, false); _landscapeToolbarView.cancelPressed = toolbarCancelPressed; _landscapeToolbarView.donePressed = toolbarDonePressed; _landscapeToolbarView.doneLongPressed = toolbarDoneLongPressed; @@ -641,17 +656,17 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * - (UIView *)timerButton { if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) - return [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab]; + return [_portraitToolbarView viewForTab:TGPhotoEditorTimerTab]; else - return [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab]; + return [_landscapeToolbarView viewForTab:TGPhotoEditorTimerTab]; } - (UIView *)qualityButton { if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) - return [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab]; + return [_portraitToolbarView viewForTab:TGPhotoEditorQualityTab]; else - return [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab]; + return [_landscapeToolbarView viewForTab:TGPhotoEditorQualityTab]; } - (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel @@ -1188,19 +1203,15 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _muteButton.selected = adjustments.sendAsGif; - TGPhotoEditorButton *qualityButton = [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab]; + UIView *qualityButton = [_portraitToolbarView viewForTab:TGPhotoEditorQualityTab]; if (qualityButton != nil) { bool isPhoto = [_currentItemView isKindOfClass:[TGMediaPickerGalleryPhotoItemView class]] || [_currentItem isKindOfClass:[TGCameraCapturedPhoto class]]; + bool isHd = false; + TGMediaVideoConversionPreset preset = TGMediaVideoConversionPresetCompressedMedium; if (isPhoto) { - bool isHd = _editingContext.isHighQualityPhoto; - UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForHighQuality:isHd filled: false]; - qualityButton.iconImage = icon; - - qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab]; - qualityButton.iconImage = icon; + isHd = _editingContext.isHighQualityPhoto; } else { - TGMediaVideoConversionPreset preset = 0; TGMediaVideoConversionPreset adjustmentsPreset = TGMediaVideoConversionPresetCompressedDefault; if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]]) adjustmentsPreset = ((TGMediaVideoEditAdjustments *)adjustments).preset; @@ -1221,28 +1232,19 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * TGMediaVideoConversionPreset bestPreset = [TGMediaVideoConverter bestAvailablePresetForDimensions:dimensions]; if (preset > bestPreset) preset = bestPreset; - - UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForPreset:preset]; - qualityButton.iconImage = icon; - - qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab]; - qualityButton.iconImage = icon; } + + [_portraitToolbarView setQualityButtonIsPhoto:isPhoto highQuality:isHd videoPreset:preset]; + [_landscapeToolbarView setQualityButtonIsPhoto:isPhoto highQuality:isHd videoPreset:preset]; } - TGPhotoEditorButton *timerButton = [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab]; + UIView *timerButton = [_portraitToolbarView viewForTab:TGPhotoEditorTimerTab]; if (timerButton != nil) { NSInteger value = [timer integerValue]; - UIImage *defaultIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:0]; - UIImage *icon = [TGPhotoEditorInterfaceAssets timerIconForValue:value]; - [timerButton setIconImage:defaultIcon activeIconImage:icon]; - - TGPhotoEditorButton *landscapeTimerButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab]; - - timerButton = landscapeTimerButton; - [timerButton setIconImage:defaultIcon activeIconImage:icon]; + [_portraitToolbarView setTimerButtonValue:value]; + [_landscapeToolbarView setTimerButtonValue:value]; if (value > 0) highlightedButtons |= TGPhotoEditorTimerTab; @@ -1413,6 +1415,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _coverButton.alpha = alpha; _arrowView.alpha = alpha * 0.6f; _recipientLabel.alpha = alpha * 0.6; + _captionMixin.livePhotoButtonView.alpha = alpha; } completion:^(BOOL finished) { if (finished) @@ -1420,6 +1423,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _checkButton.userInteractionEnabled = !hidden; _muteButton.userInteractionEnabled = !hidden; _coverButton.userInteractionEnabled = !hidden; + _captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden; } }]; @@ -1448,6 +1452,9 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _arrowView.alpha = alpha * 0.6f; _recipientLabel.alpha = alpha * 0.6; + + _captionMixin.livePhotoButtonView.alpha = alpha; + _captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden; } if (hidden) @@ -1735,11 +1742,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * { [self setSelectionInterfaceHidden:true animated:true]; - [UIView animateWithDuration:0.2 animations:^ + [UIView animateWithDuration:0.3 animations:^ { _captionMixin.inputPanelView.alpha = 0.0f; - _portraitToolbarView.doneButton.alpha = 0.0f; - _landscapeToolbarView.doneButton.alpha = 0.0f; + _portraitToolbarView.alpha = 0.0f; + _landscapeToolbarView.alpha = 0.0f; }]; } @@ -1747,11 +1754,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * { [self setSelectionInterfaceHidden:false animated:true]; - [UIView animateWithDuration:0.3 animations:^ + [UIView animateWithDuration:0.2 animations:^ { _captionMixin.inputPanelView.alpha = 1.0f; - _portraitToolbarView.doneButton.alpha = 1.0f; - _landscapeToolbarView.doneButton.alpha = 1.0f; + _portraitToolbarView.alpha = 1.0f; + _landscapeToolbarView.alpha = 1.0f; }]; } @@ -1861,7 +1868,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * break; default: - frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 26 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height); + frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 26 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 74.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height); break; } @@ -1972,7 +1979,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * break; default: - frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 45 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), 44, 44); + frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 50 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), 44, 44); break; } @@ -2085,6 +2092,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * CGFloat screenSide = MAX(screenSize.width, screenSize.height); UIEdgeInsets screenEdges = UIEdgeInsetsZero; + _portraitToolbarView.bottomInset = _safeAreaInset.bottom; + if (TGIsPad()) { _landscapeToolbarView.hidden = true; @@ -2118,7 +2127,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _coverTitleLabel.frame = CGRectMake(screenEdges.left + floor((self.frame.size.width - _coverTitleLabel.frame.size.width) / 2.0), coverTitleTopY + 26, _coverTitleLabel.frame.size.width, _coverTitleLabel.frame.size.height); UIEdgeInsets captionEdgeInsets = screenEdges; - captionEdgeInsets.bottom = _portraitToolbarView.frame.size.height; + captionEdgeInsets.bottom = _portraitToolbarView.frame.size.height + 10.0; [_captionMixin updateLayoutWithFrame:self.bounds edgeInsets:captionEdgeInsets animated:false]; switch (orientation) @@ -2157,14 +2166,14 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * { [UIView performWithoutAnimation:^ { - _photoCounterButton.frame = CGRectMake(screenEdges.right - 56 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 40 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), 64, 38); + _photoCounterButton.frame = CGRectMake(screenEdges.right - 64 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 50 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), 64, 38); - _selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 54 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize); + _selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 64 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize); }]; _landscapeToolbarView.frame = CGRectMake(_landscapeToolbarView.frame.origin.x, screenEdges.top, TGPhotoEditorToolbarSize, self.frame.size.height); - _headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 64.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 72.0); + _headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 74.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 72.0); } break; } diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index 35a79b3278..12edfa97bd 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -142,7 +142,7 @@ strongSelf.completeWithItem(item, false, 0); }; - model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) { + model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) { __strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf; if (strongSelf == nil || !(hasSilentPosting || hasSchedule)) return; @@ -236,6 +236,19 @@ strongSelf.completeWithItem(item, false, 0); }); }; + if (sourceView != nil && strongSelf->_stickersContext.presentMediaPickerSendActionMenu != nil && strongSelf->_stickersContext.presentMediaPickerSendActionMenu(sourceView, hasSilentPosting, effectiveHasSchedule, effectiveHasSchedule, reminder, false, ^{ + if (controller.sendSilently != nil) + controller.sendSilently(); + }, ^{ + if (controller.sendWhenOnline != nil) + controller.sendWhenOnline(); + }, ^{ + if (controller.schedule != nil) + controller.schedule(); + }, ^{ + })) { + return; + } TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:[strongSelf->_context makeOverlayWindowManager] parentController:strongSelf->_parentController contentController:controller]; controllerWindow.hidden = false; diff --git a/submodules/LegacyComponents/Sources/TGPhotoCropController.h b/submodules/LegacyComponents/Sources/TGPhotoCropController.h index 1a32504b05..6d6a873680 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCropController.h +++ b/submodules/LegacyComponents/Sources/TGPhotoCropController.h @@ -21,6 +21,7 @@ - (void)rotate; - (void)mirror; - (void)aspectRatioButtonPressed; +- (void)aspectRatioButtonPressedWithSourceView:(UIView *)sourceView; - (void)setImage:(UIImage *)image; - (void)setSnapshotImage:(UIImage *)snapshotImage; diff --git a/submodules/LegacyComponents/Sources/TGPhotoCropController.m b/submodules/LegacyComponents/Sources/TGPhotoCropController.m index f197b0418f..eb854e0fa7 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCropController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCropController.m @@ -22,8 +22,6 @@ #import "TGPhotoCropView.h" #import -#import - const CGFloat TGPhotoCropButtonsWrapperSize = 61.0f; const CGSize TGPhotoCropAreaInsetSize = { 9, 9 }; @@ -535,6 +533,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; } - (void)aspectRatioButtonPressed +{ + [self aspectRatioButtonPressedWithSourceView:nil]; +} + +- (void)aspectRatioButtonPressedWithSourceView:(UIView *)sourceView { if (_cropView.isAnimating) return; @@ -547,16 +550,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; { [_cropView performConfirmAnimated:true]; - TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false]; - controller.dismissesByOutsideTap = true; - controller.hasSwipeGesture = true; - __weak TGMenuSheetController *weakController = controller; __weak TGPhotoCropController *weakSelf = self; void (^action)(NSString *) = ^(NSString *ratioString) { __strong TGPhotoCropController *strongSelf = weakSelf; - __strong TGMenuSheetController *strongController = weakController; if (strongSelf == nil) return; @@ -569,7 +567,7 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; else { aspectRatio = [ratioString floatValue]; - if (_cropView.cropOrientation == UIImageOrientationLeft || _cropView.cropOrientation == UIImageOrientationRight) + if (strongSelf->_cropView.cropOrientation == UIImageOrientationLeft || strongSelf->_cropView.cropOrientation == UIImageOrientationRight) aspectRatio = 1.0f / aspectRatio; } @@ -588,13 +586,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; else TGDispatchAfter(0.1f, dispatch_get_main_queue(), setAspectRatioBlock); #pragma clang diagnostic pop - - [strongController dismissAnimated:true]; }; - NSMutableArray *items = [[NSMutableArray alloc] init]; - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(TGPhotoCropOriginalAspectRatio); }]]; - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(@"1.0"); }]]; + NSMutableArray *actions = [[NSMutableArray alloc] init]; + [actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") action:TGPhotoCropOriginalAspectRatio]]; + [actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") action:@"1.0"]]; CGSize croppedImageSize = _cropView.cropRect.size; if (_cropView.cropOrientation == UIImageOrientationLeft || _cropView.cropOrientation == UIImageOrientationRight) @@ -634,26 +630,14 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original"; ratio = heightComponent / widthComponent; - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action([NSString stringWithFormat:@"%f", ratio]); }]]; + [actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] action:[NSString stringWithFormat:@"%f", ratio]]]; } - [items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ + [_context presentActionSheet:actions view:(sourceView ?: self.view) completion:^(LegacyComponentsActionSheetAction *selectedAction) { - __strong TGMenuSheetController *strongController = weakController; - if (strongController != nil) - [strongController dismissAnimated:true]; - }]]; - - [controller setItemViews:items]; -// controller.sourceRect = ^CGRect -// { -// __strong TGPhotoCropController *strongSelf = weakSelf; -// if (strongSelf != nil) -// return [strongSelf.view convertRect:strongSelf->_aspectRatioButton.frame fromView:strongSelf->_aspectRatioButton.superview]; -// -// return CGRectZero; -// }; - [controller presentInViewController:self.parentViewController sourceView:self.view animated:true]; + if (selectedAction != nil) + action(selectedAction.action); + }]; } [self _updateTabs]; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorBlurToolView.m b/submodules/LegacyComponents/Sources/TGPhotoEditorBlurToolView.m index c85605aee8..0fdaa96527 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorBlurToolView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorBlurToolView.m @@ -169,25 +169,7 @@ - (void)blurButtonPressed:(TGPhotoEditorBlurTypeButton *)sender { -// if (sender.tag != 0 && sender.tag == _currentType) -// { -// _editingIntensity = true; -// _startIntensity = [(PGBlurToolValue *)self.value intensity]; -// -// PGBlurToolValue *value = [(PGBlurToolValue *)self.value copy]; -// value.editingIntensity = true; -// -// _value = value; -// -// if (self.valueChanged != nil) -// self.valueChanged(value); -// -// [self setIntensitySliderHidden:false animated:true]; -// } -// else -// { - [self setSelectedBlurType:(PGBlurToolType)sender.tag update:true]; -// } + [self setSelectedBlurType:(PGBlurToolType)sender.tag update:true]; } - (void)setValue:(id)value diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index f61c9a9f19..c35be4ef10 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -27,6 +27,7 @@ #import #import +#import #import "TGPhotoEditorPreviewView.h" #import @@ -46,11 +47,21 @@ #import "TGMediaPickerGalleryVideoScrubber.h" #import "TGMediaPickerGalleryVideoScrubberThumbnailView.h" -#import - #import #import +static UIView *TGPhotoEditorCreatePhotoToolbarView(id context, TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, id stickersContext) +{ + if (stickersContext.photoToolbarView != nil) + { + UIView *toolbarView = stickersContext.photoToolbarView(backButton, doneButton, solidBackground, false); + if (toolbarView != nil) + return toolbarView; + } + + return [[TGPhotoToolbarView alloc] initWithContext:context backButton:backButton doneButton:doneButton solidBackground:solidBackground stickersContext:nil]; +} + @interface TGPhotoEditorController () { bool _switchingTab; @@ -64,8 +75,8 @@ UIView *_containerView; UIView *_wrapperView; UIView *_transitionWrapperView; - TGPhotoToolbarView *_portraitToolbarView; - TGPhotoToolbarView *_landscapeToolbarView; + UIView *_portraitToolbarView; + UIView *_landscapeToolbarView; TGPhotoEditorPreviewView *_previewView; PGPhotoEditorView *_fullPreviewView; UIView *_fullEntitiesView; @@ -285,16 +296,27 @@ case TGPhotoEditorRotateTab: case TGPhotoEditorMirrorTab: - case TGPhotoEditorAspectRatioTab: if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoCropController class]] || [strongSelf->_currentTabController isKindOfClass:[TGPhotoAvatarPreviewController class]]) [strongSelf->_currentTabController handleTabAction:tab]; break; + + case TGPhotoEditorAspectRatioTab: + if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoCropController class]]) + { + UIView *toolbarView = UIInterfaceOrientationIsPortrait(strongSelf.effectiveOrientation) ? strongSelf->_portraitToolbarView : strongSelf->_landscapeToolbarView; + [(TGPhotoCropController *)strongSelf->_currentTabController aspectRatioButtonPressedWithSourceView:[toolbarView viewForTab:TGPhotoEditorAspectRatioTab]]; + } + else if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoAvatarPreviewController class]]) + { + [strongSelf->_currentTabController handleTabAction:tab]; + } + break; } }; TGPhotoEditorBackButton backButton = TGPhotoEditorBackButtonCancel; TGPhotoEditorDoneButton doneButton = TGPhotoEditorDoneButtonCheck; - _portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil]; + _portraitToolbarView = TGPhotoEditorCreatePhotoToolbarView(_context, backButton, doneButton, true, _stickersContext); [_portraitToolbarView setToolbarTabs:_availableTabs animated:false]; [_portraitToolbarView setActiveTab:_currentTab]; _portraitToolbarView.cancelPressed = toolbarCancelPressed; @@ -303,7 +325,7 @@ _portraitToolbarView.tabPressed = toolbarTabPressed; [_wrapperView addSubview:_portraitToolbarView]; - _landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil]; + _landscapeToolbarView = TGPhotoEditorCreatePhotoToolbarView(_context, backButton, doneButton, true, _stickersContext); [_landscapeToolbarView setToolbarTabs:_availableTabs animated:false]; [_landscapeToolbarView setActiveTab:_currentTab]; _landscapeToolbarView.cancelPressed = toolbarCancelPressed; @@ -1974,34 +1996,12 @@ if ((_initialAdjustments == nil && (![editorValues isDefaultValuesForAvatar:[self presentedForAvatarCreation]] || editorValues.cropOrientation != UIImageOrientationUp)) || (_initialAdjustments != nil && ![editorValues isEqual:_initialAdjustments])) { - TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false]; - controller.dismissesByOutsideTap = true; - controller.narrowInLandscape = true; - __weak TGMenuSheetController *weakController = controller; - - NSArray *items = @ + NSArray *actions = @ [ - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^ - { - __strong TGMenuSheetController *strongController = weakController; - if (strongController == nil) - return; - - [strongController dismissAnimated:true manual:false completion:^ - { - dismiss(); - }]; - }], - [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^ - { - __strong TGMenuSheetController *strongController = weakController; - if (strongController != nil) - [strongController dismissAnimated:true]; - }] + [[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") action:@"discard" type:LegacyComponentsActionSheetActionTypeDestructive] ]; - [controller setItemViews:items]; - controller.sourceRect = ^ + [_context presentActionSheet:actions view:self.view sourceRect:^CGRect { __strong TGPhotoEditorController *strongSelf = weakSelf; if (strongSelf == nil) @@ -2011,8 +2011,11 @@ return [strongSelf.view convertRect:strongSelf->_portraitToolbarView.cancelButtonFrame fromView:strongSelf->_portraitToolbarView]; else return [strongSelf.view convertRect:strongSelf->_landscapeToolbarView.cancelButtonFrame fromView:strongSelf->_landscapeToolbarView]; - }; - [controller presentInViewController:self sourceView:self.view animated:true]; + } completion:^(LegacyComponentsActionSheetAction *selectedAction) + { + if ([selectedAction.action isEqualToString:@"discard"]) + dismiss(); + }]; } else { diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m b/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m index 9ab2a97410..8a22a46a4c 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorInterfaceAssets.m @@ -30,11 +30,11 @@ + (UIColor *)accentColor { - TGMediaAssetsPallete *pallete = nil; - if ([[LegacyComponentsGlobals provider] respondsToSelector:@selector(mediaAssetsPallete)]) - pallete = [[LegacyComponentsGlobals provider] mediaAssetsPallete]; - - return pallete.maybeAccentColor ?: UIColorRGB(0x65b3ff); +// TGMediaAssetsPallete *pallete = nil; +// if ([[LegacyComponentsGlobals provider] respondsToSelector:@selector(mediaAssetsPallete)]) +// pallete = [[LegacyComponentsGlobals provider] mediaAssetsPallete]; +// + return UIColorRGB(0xffd300); //pallete.maybeAccentColor ?: UIColorRGB(0x65b3ff); } + (UIColor *)panelBackgroundColor @@ -374,7 +374,7 @@ + (UIFont *)editorItemTitleFont { - return [TGFont systemFontOfSize:14]; + return [TGFont boldSystemFontOfSize:13]; //[TGFont systemFontOfSize:1]; } + (UIColor *)filterSelectionColor diff --git a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m index 8b519b5860..2c98f01c3b 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoToolbarView.m @@ -31,6 +31,7 @@ bool _animatingCancelDoneButtons; } + @end @implementation TGPhotoToolbarView @@ -635,6 +636,34 @@ return nil; } +- (UIView *)viewForTab:(TGPhotoEditorTab)tab +{ + return [self buttonForTab:tab]; +} + +- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset +{ + TGPhotoEditorButton *qualityButton = [self buttonForTab:TGPhotoEditorQualityTab]; + if (qualityButton == nil) + return; + + if (isPhoto) + qualityButton.iconImage = [TGPhotoEditorInterfaceAssets qualityIconForHighQuality:highQuality filled:false]; + else + qualityButton.iconImage = [TGPhotoEditorInterfaceAssets qualityIconForPreset:(TGMediaVideoConversionPreset)videoPreset]; +} + +- (void)setTimerButtonValue:(NSInteger)value +{ + TGPhotoEditorButton *timerButton = [self buttonForTab:TGPhotoEditorTimerTab]; + if (timerButton == nil) + return; + + UIImage *defaultIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:0]; + UIImage *activeIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:value]; + [timerButton setIconImage:defaultIcon activeIconImage:activeIcon]; +} + - (void)layoutSubviews { CGRect backgroundFrame = self.bounds; diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index c407675ed9..b894c687d6 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -236,7 +236,7 @@ [strongController dismissWhenReadyAnimated:true]; }; - model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) + model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) { __strong TGModernGalleryController *strongController = weakGalleryController; __strong TGMediaPickerGalleryModel *strongModel = weakModel; @@ -286,6 +286,19 @@ complete(silentPosting, time); }); }; + if (sourceView != nil && stickersContext.presentMediaPickerSendActionMenu != nil && stickersContext.presentMediaPickerSendActionMenu(sourceView, hasSilentPosting, hasSchedule, hasSchedule, reminder, false, ^{ + if (sendController.sendSilently != nil) + sendController.sendSilently(); + }, ^{ + if (sendController.sendWhenOnline != nil) + sendController.sendWhenOnline(); + }, ^{ + if (sendController.schedule != nil) + sendController.schedule(); + }, ^{ + })) { + return; + } [strongController presentViewController:sendController animated:false completion:nil]; }; diff --git a/submodules/LegacyMediaPickerUI/BUILD b/submodules/LegacyMediaPickerUI/BUILD index 7cdc998863..951f5a14ff 100644 --- a/submodules/LegacyMediaPickerUI/BUILD +++ b/submodules/LegacyMediaPickerUI/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/LegacyComponents:LegacyComponents", "//submodules/LegacyUI:LegacyUI", + "//submodules/ContextUI:ContextUI", "//submodules/MimeTypes:MimeTypes", "//submodules/LocalMediaResources:LocalMediaResources", "//submodules/SearchPeerMembers:SearchPeerMembers", diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index c3e477107c..87c5001af8 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -166,6 +166,7 @@ public func legacyMediaEditor( snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, + photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)? = nil, hasSilentPosting: Bool = false, hasSchedule: Bool = false, reminder: Bool = false, @@ -191,6 +192,7 @@ public func legacyMediaEditor( paintStickersContext.captionPanelView = { return getCaptionPanelView() } + paintStickersContext.photoToolbarView = photoToolbarView let presentationData = context.sharedContext.currentPresentationData.with { $0 } let recipientName: String @@ -208,15 +210,28 @@ public func legacyMediaEditor( legacyController.blocksBackgroundWhenInOverlay = true legacyController.acceptsFocusWhenInOverlay = true legacyController.statusBar.statusBarStyle = .Ignore + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: presentationData, presentInGlobalOverlay: { [weak legacyController] controller in + if let legacyController { + legacyController.presentInGlobalOverlay(controller) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(controller) + } else { + context.sharedContext.presentGlobalController(controller, nil) + } + }) legacyController.controllerLoaded = { [weak legacyController] in legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true } - + legacyController.presentationCompleted = { + Queue.mainQueue().after(0.1) { + transitionCompletion?() + } + } + let schedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { media, done in presentSchedulePicker(media, done) } let appeared: () -> Void = { - transitionCompletion?() } let completion: (TGMediaEditableItem, TGMediaEditingContext, Bool, Int32) -> Void = { result, editingContext, silentPosting, scheduleTime in let nativeGenerator = legacyAssetPickerItemGenerator() @@ -235,64 +250,30 @@ public func legacyMediaEditor( legacyController.enableSizeClassSignal = true - if isGif { - let galleryController = TGPhotoVideoEditor.controller( - with: legacyController.context, - caption: initialCaption, - withItem: item, - paint: mode == .draw, - adjustments: mode == .adjustments, - recipientName: recipientName, - stickersContext: paintStickersContext, - from: .zero, - mainSnapshot: nil, - snapshots: snapshots as [Any], - immediate: transitionCompletion != nil, - activateInput: mode == .caption, - isGif: true, - hasSilentPosting: hasSilentPosting, - hasSchedule: hasSchedule, - reminder: reminder, - presentSchedulePicker: schedulePicker, - appeared: appeared, - completion: completion, - dismissed: dismissed - ) - legacyController.bind(controller: galleryController) - present(legacyController, nil) - } else { - let emptyController = LegacyEmptyController(context: legacyController.context)! - emptyController.navigationBarShouldBeHidden = true - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - legacyController.bind(controller: navigationController) - - present(legacyController, nil) - - TGPhotoVideoEditor.present( - with: legacyController.context, - controller: emptyController, - caption: initialCaption, - withItem: item, - paint: mode == .draw, - adjustments: mode == .adjustments, - recipientName: recipientName, - stickersContext: paintStickersContext, - from: .zero, - mainSnapshot: nil, - snapshots: snapshots as [Any], - immediate: transitionCompletion != nil, - activateInput: mode == .caption, - isGif: false, - hasSilentPosting: hasSilentPosting, - hasSchedule: hasSchedule, - reminder: reminder, - presentSchedulePicker: schedulePicker, - appeared: appeared, - completion: completion, - dismissed: dismissed - ) - } + let galleryController = TGPhotoVideoEditor.controller( + with: legacyController.context, + caption: initialCaption, + withItem: item, + paint: mode == .draw, + adjustments: mode == .adjustments, + recipientName: recipientName, + stickersContext: paintStickersContext, + from: .zero, + mainSnapshot: nil, + snapshots: snapshots as [Any], + immediate: transitionCompletion != nil, + activateInput: mode == .caption, + isGif: isGif, + hasSilentPosting: hasSilentPosting, + hasSchedule: hasSchedule, + reminder: reminder, + presentSchedulePicker: schedulePicker, + appeared: appeared, + completion: completion, + dismissed: dismissed + ) + legacyController.bind(controller: galleryController) + present(legacyController, nil) }) } @@ -389,6 +370,15 @@ public func legacyAttachmentMenu( } let paintStickersContext = LegacyPaintStickersContext(context: context) + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: updatedPresentationData.initial, presentInGlobalOverlay: { [weak parentController] controller in + if let parentController { + parentController.presentInGlobalOverlay(controller) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(controller) + } else { + context.sharedContext.presentGlobalController(controller, nil) + } + }) paintStickersContext.captionPanelView = { return getCaptionPanelView() } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index b11536ead8..4d452670b9 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -21,6 +21,7 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String { public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: EngineRawPeer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) { let paintStickersContext = LegacyPaintStickersContext(context: context) + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context) paintStickersContext.captionPanelView = { return getCaptionPanelView() } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index f10578a5ff..bdf89b9fc7 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -13,6 +13,7 @@ import SolidRoundedButtonNode import MediaEditor import DrawingUI import TelegramPresentationData +import ContextUI import AnimatedCountLabelNode import CoreMedia @@ -571,15 +572,118 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender } } +public typealias LegacyMediaPickerSendActionMenuPresenter = ( + UIView, + Bool, + Bool, + Bool, + Bool, + Bool, + @escaping () -> Void, + @escaping () -> Void, + @escaping () -> Void, + @escaping () -> Void +) -> Bool + +private final class LegacyMediaPickerSendActionMenuReferenceContentSource: ContextReferenceContentSource { + private weak var sourceView: UIView? + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + guard let sourceView = self.sourceView else { + return nil + } + return ContextControllerReferenceViewInfo(referenceView: sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top) + } +} + +public func makeLegacyMediaPickerSendActionMenuPresenter( + context: AccountContext, + presentationData: PresentationData? = nil, + presentInGlobalOverlay: ((ViewController) -> Void)? = nil +) -> LegacyMediaPickerSendActionMenuPresenter { + return { sourceView, canSendSilently, canSendWhenOnline, canSchedule, reminder, hasTimer, sendSilently, sendWhenOnline, schedule, sendWithTimer in + guard sourceView.window != nil else { + return false + } + + let presentationData = (presentationData ?? context.sharedContext.currentPresentationData.with { $0 }).withUpdated(theme: defaultDarkPresentationTheme) + var items: [ContextMenuItem] = [] + + if canSendSilently { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + sendSilently() + }))) + } + + if canSendWhenOnline { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + sendWhenOnline() + }))) + } + + if canSchedule { + items.append(.action(ContextMenuActionItem(text: reminder ? presentationData.strings.Conversation_SendMessage_SetReminder : presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + schedule() + }))) + } + + if hasTimer { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_Timer_Send, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Timer"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + sendWithTimer() + }))) + } + + guard !items.isEmpty else { + return false + } + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(LegacyMediaPickerSendActionMenuReferenceContentSource(sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + if let presentInGlobalOverlay { + presentInGlobalOverlay(contextController) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(contextController) + } else { + context.sharedContext.presentGlobalController(contextController, nil) + } + return true + } +} + public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersContext { public var captionPanelView: (() -> TGCaptionPanelView?)? public var livePhotoButton: (() -> TGLivePhotoButton?)? + public var photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)? + public var presentMediaPickerSendActionMenu: LegacyMediaPickerSendActionMenuPresenter? public var editCover: ((CGSize, @escaping (UIImage) -> Void) -> Void)? - + private let context: AccountContext - + public init(context: AccountContext) { self.context = context + super.init() + + self.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context) } class LegacyDrawingAdapter: NSObject, TGPhotoDrawingAdapter { diff --git a/submodules/LegacyUI/BUILD b/submodules/LegacyUI/BUILD index 958e606004..9fd41d27b1 100644 --- a/submodules/LegacyUI/BUILD +++ b/submodules/LegacyUI/BUILD @@ -14,6 +14,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/TelegramCore:TelegramCore", "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/ContextUI:ContextUI", "//submodules/Display:Display", "//submodules/AccountContext:AccountContext", "//submodules/TelegramAudio:TelegramAudio", diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index 0259dd86b6..7b276de96e 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import ContextUI import Display import SSignalKit import SwiftSignalKit @@ -28,6 +29,18 @@ private func passControllerAppearanceAnimated(in: Bool, presentation: LegacyCont } } +private final class LegacyActionSheetContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top) + } +} + private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyComponentsOverlayWindowManager { private weak var contentController: UIViewController? private weak var parentController: ViewController? @@ -260,11 +273,80 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext { } public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) { - + self.presentActionSheet(actions, view: view, sourceRect: nil, completion: completion) } public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, sourceRect: (() -> CGRect)!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) { + guard let controller = self.controller, let view = view else { + completion?(nil) + return + } + let presentationData: PresentationData + if let context = legacyContextGet() { + presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + } else { + presentationData = defaultPresentationData().withUpdated(theme: defaultDarkColorPresentationTheme) + } + + let anchorView: UIView? + let referenceView: UIView + if let sourceRect = sourceRect { + let anchor = UIView(frame: sourceRect()) + anchor.isUserInteractionEnabled = false + anchor.backgroundColor = .clear + view.addSubview(anchor) + anchorView = anchor + referenceView = anchor + } else { + anchorView = nil + referenceView = view + } + + var didSelectAction = false + var items: [ContextMenuItem] = [] + for legacyAction in actions ?? [] { + if legacyAction.type == LegacyComponentsActionSheetActionTypeCancel { + continue + } + guard let title = legacyAction.title else { + continue + } + let textColor: ContextMenuActionItemTextColor = legacyAction.type == LegacyComponentsActionSheetActionTypeDestructive ? .destructive : .primary + items.append(.action(ContextMenuActionItem(text: title, textColor: textColor, icon: { _ in + return nil + }, action: { actionContext in + didSelectAction = true + if let contextController = actionContext.controller { + contextController.dismiss(result: .default, completion: { + completion?(legacyAction) + }) + } else { + anchorView?.removeFromSuperview() + completion?(legacyAction) + } + }))) + } + + if items.isEmpty { + anchorView?.removeFromSuperview() + completion?(nil) + return + } + + let contextController = makeContextController( + context: legacyContextGet(), + presentationData: presentationData, + source: .reference(LegacyActionSheetContextReferenceContentSource(sourceView: referenceView)), + items: .single(ContextController.Items(content: .list(items))) + ) + contextController.dismissed = { [weak anchorView] in + anchorView?.removeFromSuperview() + if !didSelectAction { + completion?(nil) + } + } + controller.present(contextController, in: .window(.root)) } public func presentTooltip(_ text: String!, icon: UIImage!, sourceRect: CGRect) { diff --git a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift index c8d8e43912..0a5f677cf3 100644 --- a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift +++ b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift @@ -4,6 +4,10 @@ import AsyncDisplayKit import Display import TelegramPresentationData +private let compactInfinityFont = Font.with(size: 14.0, design: .round, weight: .bold) +private let compactTextFont = Font.with(size: 12.0, design: .round, weight: .bold) +private let compactSmallTextFont = Font.with(size: 10.0, design: .round, weight: .bold) + private let infinityFont = Font.with(size: 15.0, design: .round, weight: .bold) private let textFont = Font.with(size: 13.0, design: .round, weight: .bold) private let smallTextFont = Font.with(size: 11.0, design: .round, weight: .bold) @@ -134,11 +138,11 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { let font: UIFont if parameters.string == "∞" { - font = infinityFont + font = bounds.width < 28.0 ? compactInfinityFont : infinityFont } else if parameters.string.count > 2 { - font = smallTextFont + font = bounds.width < 28.0 ? compactSmallTextFont : smallTextFont } else { - font = textFont + font = bounds.width < 28.0 ? compactTextFont : textFont } let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: parameters.foregroundColor] @@ -147,7 +151,7 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { var offset = CGPoint() if parameters.string == "∞" { - offset = CGPoint(x: 1.0, y: -1.0) + offset = bounds.width < 28.0 ? CGPoint(x: 1.0 - UIScreenPixel, y: 0.0) : CGPoint(x: 1.0, y: -1.0) } else if parameters.string.count > 2 { offset = CGPoint(x: 0.0, y: UIScreenPixel) } diff --git a/submodules/LocationUI/BUILD b/submodules/LocationUI/BUILD index 321fd7dd3a..39d33fd6d0 100644 --- a/submodules/LocationUI/BUILD +++ b/submodules/LocationUI/BUILD @@ -16,6 +16,7 @@ swift_library( "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/LegacyComponents:LegacyComponents", + "//submodules/ContextUI:ContextUI", "//submodules/ShareController:ShareController", "//submodules/AccountContext:AccountContext", "//submodules/OpenInExternalAppUI:OpenInExternalAppUI", @@ -41,14 +42,20 @@ swift_library( "//submodules/TelegramNotices:TelegramNotices", "//submodules/TooltipUI:TooltipUI", "//submodules/UndoUI:UndoUI", + "//submodules/Weather", "//submodules/AttachmentUI:AttachmentUI", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/ComponentFlow", "//submodules/Components/BundleIconComponent", + "//submodules/Components/SheetComponent", + "//submodules/Components/ViewControllerComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/GlassControls", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/SearchInputPanelComponent", "//submodules/TelegramUI/Components/ButtonComponent", diff --git a/submodules/LocationUI/Sources/LocationActionListItem.swift b/submodules/LocationUI/Sources/LocationActionListItem.swift index ea37651e03..a4c5121a60 100644 --- a/submodules/LocationUI/Sources/LocationActionListItem.swift +++ b/submodules/LocationUI/Sources/LocationActionListItem.swift @@ -141,16 +141,18 @@ final class LocationActionListItem: ListViewItem { let title: String let subtitle: String let icon: LocationActionListItemIcon + let isOpaque: Bool let beginTimeAndTimeout: (Double, Double)? let action: () -> Void let highlighted: (Bool) -> Void - public init(presentationData: ItemListPresentationData, engine: TelegramEngine, title: String, subtitle: String, icon: LocationActionListItemIcon, beginTimeAndTimeout: (Double, Double)?, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) { + public init(presentationData: ItemListPresentationData, engine: TelegramEngine, title: String, subtitle: String, icon: LocationActionListItemIcon, isOpaque: Bool = true, beginTimeAndTimeout: (Double, Double)?, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) { self.presentationData = presentationData self.engine = engine self.title = title self.subtitle = subtitle self.icon = icon + self.isOpaque = isOpaque self.beginTimeAndTimeout = beginTimeAndTimeout self.action = action self.highlighted = highlighted @@ -216,6 +218,7 @@ final class LocationActionListItemNode: ListViewItemNode { self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.clipsToBounds = true self.highlightedBackgroundNode.isLayerBacked = true self.iconNode = ASImageNode() @@ -232,6 +235,21 @@ final class LocationActionListItemNode: ListViewItemNode { self.addSubnode(self.venueIconNode) } + func liveLocationContextSourceView(extend: Bool) -> UIView? { + guard let icon = self.item?.icon else { + return nil + } + + switch icon { + case .liveLocation: + return extend ? nil : self.view + case .extendLiveLocation: + return extend ? self.view : nil + default: + return nil + } + } + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = self.item { let makeLayout = self.asyncLayout() @@ -278,7 +296,7 @@ final class LocationActionListItemNode: ListViewItemNode { let iconLayout = self.venueIconNode.asyncLayout() return { [weak self] item, params, hasSeparator in - let leftInset: CGFloat = 65.0 + params.leftInset + let leftInset: CGFloat = (item.isOpaque ? 65.0 : 72.0 ) + params.leftInset let rightInset: CGFloat = params.rightInset let verticalInset: CGFloat = 8.0 let iconSize: CGFloat = 40.0 @@ -292,7 +310,7 @@ final class LocationActionListItemNode: ListViewItemNode { let subtitleAttributedString = NSAttributedString(string: item.subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let titleSpacing: CGFloat = 1.0 + let titleSpacing: CGFloat = 0.0 let bottomInset: CGFloat = hasSeparator ? 0.0 : 4.0 var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset) if hasSeparator { @@ -300,6 +318,8 @@ final class LocationActionListItemNode: ListViewItemNode { } let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) + var hasSeparator = hasSeparator + return (nodeLayout, { [weak self] in var updatedTheme: PresentationTheme? if currentItem?.presentationData.theme !== item.presentationData.theme { @@ -315,11 +335,15 @@ final class LocationActionListItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.layoutParams = params - + if let _ = updatedTheme { strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + if item.isOpaque { + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } else { + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.contextMenu.itemHighlightedBackgroundColor + } } var arguments: TransformImageCustomArguments? @@ -394,15 +418,34 @@ final class LocationActionListItemNode: ListViewItemNode { let topHighlightInset: CGFloat = separatorHeight let separatorRightInset: CGFloat = 16.0 - let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) + let contentLeftInset: CGFloat = item.isOpaque ? 0.0 : 7.0 + + let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0 + contentLeftInset, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) strongSelf.iconNode.frame = iconNodeFrame strongSelf.venueIconNode.frame = iconNodeFrame - strongSelf.wavesNode?.frame = CGRect(origin: CGPoint(x: params.leftInset + 11.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0) - 4.0), size: CGSize(width: 48.0, height: 48.0)) + strongSelf.wavesNode?.frame = CGRect(origin: CGPoint(x: params.leftInset + 11.0 + contentLeftInset, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0) - 4.0), size: CGSize(width: 48.0, height: 48.0)) strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset)) + + let highlightFrame: CGRect + let highlightCornerRadius: CGFloat + if item.isOpaque { + highlightFrame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset)) + highlightCornerRadius = 0.0 + } else { + highlightFrame = CGRect(origin: CGPoint(x: 14.0, y: 2.0), size: CGSize(width: contentSize.width - 14.0 * 2.0, height: 52.0)) + highlightCornerRadius = highlightFrame.height * 0.5 + } + + strongSelf.highlightedBackgroundNode.frame = highlightFrame + strongSelf.highlightedBackgroundNode.cornerRadius = highlightCornerRadius strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - leftInset - params.rightInset - separatorRightInset, height: separatorHeight)) + + if !item.isOpaque { + hasSeparator = false + } + strongSelf.backgroundNode.isHidden = !item.isOpaque strongSelf.separatorNode.isHidden = !hasSeparator if let (beginTimestamp, timeout) = item.beginTimeAndTimeout { @@ -414,9 +457,9 @@ final class LocationActionListItemNode: ListViewItemNode { strongSelf.addSubnode(timerNode) strongSelf.timerNode = timerNode } - let timerSize = CGSize(width: 28.0, height: 28.0) + let timerSize = CGSize(width: 24.0, height: 24.0) timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: beginTimestamp, timeout: Int32(timeout) == liveLocationIndefinitePeriod ? -1.0 : timeout, strings: item.presentationData.strings) - timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize) + timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 15.0 - contentLeftInset - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil timerNode.removeFromSupernode() diff --git a/submodules/LocationUI/Sources/LocationAnnotation.swift b/submodules/LocationUI/Sources/LocationAnnotation.swift index a9957db257..207e31cbc6 100644 --- a/submodules/LocationUI/Sources/LocationAnnotation.swift +++ b/submodules/LocationUI/Sources/LocationAnnotation.swift @@ -177,6 +177,7 @@ public class LocationPinAnnotationView: MKAnnotationView { var hasPulse = false var headingKvoToken: NSKeyValueObservation? + private var mapHeading: CGFloat = 0.0 override public class var layerClass: AnyClass { return LocationPinAnnotationLayer.self @@ -291,12 +292,10 @@ public class LocationPinAnnotationView: MKAnnotationView { headingKvoToken.invalidate() } - self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, change) in - guard let heading = change.newValue else { - return - } - self?.updateHeading(heading) + self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, _) in + self?.updateHeading() } + self.updateHeading() } else if let peer = annotation.peer { self.iconNode.isHidden = true @@ -310,7 +309,7 @@ public class LocationPinAnnotationView: MKAnnotationView { self.headingKvoToken = nil headingKvoToken.invalidate() } - self.updateHeading(nil) + self.updateHeading() } else if let location = annotation.location { let venueType = location.venue?.type ?? "" let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType) @@ -347,16 +346,23 @@ public class LocationPinAnnotationView: MKAnnotationView { self.headingKvoToken = nil headingKvoToken.invalidate() } - self.updateHeading(nil) + self.updateHeading() } } } } - private func updateHeading(_ heading: NSNumber?) { - if let heading = heading?.int32Value { + func updateMapHeading(_ mapHeading: CGFloat) { + if self.mapHeading != mapHeading { + self.mapHeading = mapHeading + self.updateHeading() + } + } + + private func updateHeading() { + if let heading = (self.annotation as? LocationPinAnnotation)?.heading?.doubleValue { self.arrowNode.isHidden = false - self.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0) + self.arrowNode.transform = CATransform3DMakeRotation((CGFloat(heading) - self.mapHeading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0) } else { self.arrowNode.isHidden = true self.arrowNode.transform = CATransform3DIdentity diff --git a/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift b/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift index 3547383093..8e56deae75 100644 --- a/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift +++ b/submodules/LocationUI/Sources/LocationDistancePickerScreen.swift @@ -1,123 +1,84 @@ import Foundation import UIKit import Display -import AsyncDisplayKit import TelegramCore import SwiftSignalKit import AccountContext -import SolidRoundedButtonNode import TelegramPresentationData import TelegramStringFormatting import PresentationDataUtils -import CoreLocation +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import ButtonComponent +import GlassBarButtonComponent +import AnimatedTextComponent +import BundleIconComponent enum LocationDistancePickerScreenStyle { case `default` case media } -final class LocationDistancePickerScreen: ViewController { - private var controllerNode: LocationDistancePickerScreenNode { - return self.displayNode as! LocationDistancePickerScreenNode - } - - private var animatedIn = false - - private let context: AccountContext - private let style: LocationDistancePickerScreenStyle - private let distances: Signal<[Double], NoError> - private let compactDisplayTitle: String? - private let updated: (Int32?) -> Void - private let completion: (Int32, @escaping () -> Void) -> Void - private let willDismiss: () -> Void - - private var presentationDataDisposable: Disposable? +final class LocationDistancePickerScreen: ViewControllerComponentContainer { + private let willDismissImpl: () -> Void + private var didCallWillDismiss = false init(context: AccountContext, style: LocationDistancePickerScreenStyle, compactDisplayTitle: String?, distances: Signal<[Double], NoError>, updated: @escaping (Int32?) -> Void, completion: @escaping (Int32, @escaping () -> Void) -> Void, willDismiss: @escaping () -> Void) { - self.context = context - self.style = style - self.distances = distances - self.compactDisplayTitle = compactDisplayTitle - self.updated = updated - self.completion = completion - self.willDismiss = willDismiss + self.willDismissImpl = willDismiss - super.init(navigationBarPresentationData: nil) - - self.statusBar.statusBarStyle = .Ignore + super.init( + context: context, + component: LocationDistancePickerScreenComponent( + context: context, + style: style, + compactDisplayTitle: compactDisplayTitle, + distances: distances, + updated: { distance in + updated(distance) + }, + completion: completion, + willDismiss: willDismiss + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore + ) self.blocksBackgroundWhenInOverlay = true - - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.controllerNode.updatePresentationData(presentationData) - } - }) - - self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - deinit { - self.presentationDataDisposable?.dispose() - } - - override public func loadDisplayNode() { - self.displayNode = LocationDistancePickerScreenNode(context: self.context, style: self.style, compactDisplayTitle: self.compactDisplayTitle, distances: self.distances) - self.controllerNode.updated = { [weak self] distance in - guard let strongSelf = self else { - return - } - strongSelf.updated(distance) - } - self.controllerNode.completion = { [weak self] distance in - guard let strongSelf = self else { - return - } - strongSelf.completion(distance, { - strongSelf.dismiss() - }) - } - self.controllerNode.dismiss = { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - } - self.controllerNode.cancel = { [weak self] in - self?.dismiss() - } - - let _ = self.controllerNode.update() - } - - override public func loadView() { - super.loadView() - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.animatedIn { - self.animatedIn = true - self.controllerNode.animateIn() + fileprivate func performWillDismissOnce() { + if self.didCallWillDismiss { + return } + self.didCallWillDismiss = true + self.willDismissImpl() } override public func dismiss(completion: (() -> Void)? = nil) { - self.willDismiss() - self.controllerNode.animateOut(completion: completion) + if let componentView = self.node.hostView.componentView as? LocationDistancePickerScreenComponent.View { + componentView.requestDismiss(completion: completion) + } else { + self.performWillDismissOnce() + super.dismiss(animated: false, completion: completion) + } } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + override public func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + if flag { + self.dismiss(completion: completion) + } else { + super.dismiss(animated: false, completion: completion) + } } } -private class TimerPickerView: UIPickerView { +private final class TimerPickerView: UIPickerView { var selectorColor: UIColor? = nil { didSet { for subview in self.subviews { @@ -151,7 +112,7 @@ private class TimerPickerView: UIPickerView { } } -private var unitValues: [Int32] = { +private let unitValues: [Int32] = { var values: [Int32] = [] for i in 0 ..< 99 { values.append(Int32(i)) @@ -159,7 +120,7 @@ private var unitValues: [Int32] = { return values }() -private var smallUnitValues: [Int32] = { +private let smallUnitValues: [Int32] = { var values: [Int32] = [] values.append(0) values.append(5) @@ -169,213 +130,318 @@ private var smallUnitValues: [Int32] = { return values }() -class LocationDistancePickerScreenNode: ViewControllerTracingNode, ASScrollViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate { - private let context: AccountContext - private let controllerStyle: LocationDistancePickerScreenStyle - private var presentationData: PresentationData - private var compactDisplayTitle: String? - private var distances: [Double] = [] +private final class LocationDistancePickerScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment - private let dimNode: ASDisplayNode - private let wrappingScrollNode: ASScrollNode - private let contentContainerNode: ASDisplayNode - private let effectNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode - private let titleNode: ASTextNode - private let textNode: ImmediateTextNode - private let cancelButton: HighlightableButtonNode - private let doneButton: SolidRoundedButtonNode + let context: AccountContext + let style: LocationDistancePickerScreenStyle + let compactDisplayTitle: String? + let distances: Signal<[Double], NoError> + let updated: (Int32) -> Void + let completion: (Int32, @escaping () -> Void) -> Void + let willDismiss: () -> Void - private let measureButtonTitleNode: ImmediateTextNode - - private var pickerView: TimerPickerView? - private let unitLabelNode: ImmediateTextNode - private let smallUnitLabelNode: ImmediateTextNode - - private var pickerTimer: SwiftSignalKit.Timer? - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - private var distancesDisposable: Disposable? - - var updated: ((Int32) -> Void)? - var completion: ((Int32) -> Void)? - var dismiss: (() -> Void)? - var cancel: (() -> Void)? - - init(context: AccountContext, style: LocationDistancePickerScreenStyle, compactDisplayTitle: String?, distances: Signal<[Double], NoError>) { + init( + context: AccountContext, + style: LocationDistancePickerScreenStyle, + compactDisplayTitle: String?, + distances: Signal<[Double], NoError>, + updated: @escaping (Int32) -> Void, + completion: @escaping (Int32, @escaping () -> Void) -> Void, + willDismiss: @escaping () -> Void + ) { self.context = context - self.controllerStyle = style - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.style = style self.compactDisplayTitle = compactDisplayTitle - - self.wrappingScrollNode = ASScrollNode() - self.wrappingScrollNode.view.alwaysBounceVertical = true - self.wrappingScrollNode.view.delaysContentTouches = false - self.wrappingScrollNode.view.canCancelContentTouches = true - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.isOpaque = false - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 - - let backgroundColor: UIColor - let textColor: UIColor - let accentColor: UIColor - let blurStyle: UIBlurEffect.Style - switch style { - case .default: - backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - textColor = self.presentationData.theme.actionSheet.primaryTextColor - accentColor = self.presentationData.theme.actionSheet.controlAccentColor - blurStyle = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark - case .media: - backgroundColor = UIColor(rgb: 0x1c1c1e) - textColor = .white - accentColor = self.presentationData.theme.actionSheet.controlAccentColor - blurStyle = .dark - } - - self.effectNode = ASDisplayNode(viewBlock: { - return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) - }) - - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor - - self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Location_ProximityNotification_Title, font: Font.bold(17.0), textColor: textColor) - - self.textNode = ImmediateTextNode() - self.textNode.alpha = 0.0 - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: accentColor, for: .normal) - - self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0) - self.doneButton.title = self.presentationData.strings.Conversation_Timer_Send - - self.unitLabelNode = ImmediateTextNode() - self.smallUnitLabelNode = ImmediateTextNode() - - self.measureButtonTitleNode = ImmediateTextNode() - - super.init() - - self.backgroundColor = nil - self.isOpaque = false - - self.unitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceKM : self.presentationData.strings.Location_ProximityNotification_DistanceMI, font: Font.regular(15.0), textColor: textColor) - self.smallUnitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceM : "", font: Font.regular(15.0), textColor: textColor) - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - self.addSubnode(self.dimNode) - - self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate - self.addSubnode(self.wrappingScrollNode) - - self.wrappingScrollNode.addSubnode(self.backgroundNode) - self.wrappingScrollNode.addSubnode(self.contentContainerNode) - - self.backgroundNode.addSubnode(self.effectNode) - self.backgroundNode.addSubnode(self.contentBackgroundNode) - self.contentContainerNode.addSubnode(self.titleNode) - self.contentContainerNode.addSubnode(self.textNode) - self.contentContainerNode.addSubnode(self.cancelButton) - self.contentContainerNode.addSubnode(self.doneButton) - - self.contentContainerNode.addSubnode(self.unitLabelNode) - self.contentContainerNode.addSubnode(self.smallUnitLabelNode) - - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.doneButton.pressed = { [weak self] in - if let strongSelf = self, let pickerView = strongSelf.pickerView { - strongSelf.doneButton.isUserInteractionEnabled = false - - let largeValue = unitValues[pickerView.selectedRow(inComponent: 0)] - let smallValue = smallUnitValues[pickerView.selectedRow(inComponent: 1)] - var value = largeValue * 1000 + smallValue * 10 - if !strongSelf.usesMetricSystem { - value = Int32(Double(value) * 1.60934) - } - strongSelf.completion?(value) - } - } - - self.setupPickerView() - - self.distancesDisposable = (distances - |> deliverOnMainQueue).start(next: { [weak self] distances in - if let strongSelf = self { - strongSelf.distances = distances - strongSelf.updateDoneButtonTitle() - } - }) + self.distances = distances + self.updated = updated + self.completion = completion + self.willDismiss = willDismiss } - deinit { - self.distancesDisposable?.dispose() - - self.pickerTimer?.invalidate() - } - - func setupPickerView() { - if let pickerView = self.pickerView { - pickerView.removeFromSuperview() - } - - let pickerView = TimerPickerView() - pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) - pickerView.dataSource = self - pickerView.delegate = self - pickerView.selectRow(0, inComponent: 0, animated: false) - - if self.usesMetricSystem { - pickerView.selectRow(6, inComponent: 1, animated: false) - } else { - pickerView.selectRow(4, inComponent: 1, animated: false) - } - self.contentContainerNode.view.addSubview(pickerView) - self.pickerView = pickerView - - self.contentContainerNode.addSubnode(self.unitLabelNode) - self.contentContainerNode.addSubnode(self.smallUnitLabelNode) - - self.pickerTimer?.invalidate() - - let pickerTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: true, completion: { [weak self] in - if let strongSelf = self { - if strongSelf.update() { - strongSelf.updateDoneButtonTitle() - } - } - }, queue: Queue.mainQueue()) - self.pickerTimer = pickerTimer - pickerTimer.start() - - self.updateDoneButtonTitle() - } - - private var usesMetricSystem: Bool { - let locale = localeWithStrings(self.presentationData.strings) - if locale.identifier.hasSuffix("GB") { + static func ==(lhs: LocationDistancePickerScreenComponent, rhs: LocationDistancePickerScreenComponent) -> Bool { + if lhs.context !== rhs.context { return false } - return locale.usesMetricSystem + if lhs.style != rhs.style { + return false + } + if lhs.compactDisplayTitle != rhs.compactDisplayTitle { + return false + } + return true } - func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 2 + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + private var component: LocationDistancePickerScreenComponent? + private var environment: ViewControllerComponentContainer.Environment? + private var isDismissed = false + private var didCallWillDismiss = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func performWillDismissOnce() { + if self.didCallWillDismiss { + return + } + self.didCallWillDismiss = true + + if let controller = self.environment?.controller() as? LocationDistancePickerScreen { + controller.performWillDismissOnce() + } else { + self.component?.willDismiss() + } + } + + func requestDismiss(completion: (() -> Void)? = nil) { + self.performWillDismissOnce() + + if self.isDismissed { + completion?() + return + } + self.isDismissed = true + + self.sheetAnimateOut.invoke(Action { [weak self] _ in + guard let self else { + completion?() + return + } + if let controller = self.environment?.controller() { + controller.dismiss(animated: false, completion: completion) + } else { + completion?() + } + }) + } + + func update(component: LocationDistancePickerScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let environment = environment[EnvironmentType.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] _ in + self?.requestDismiss() + } + ) + + let backgroundColor: SheetComponent.BackgroundColor + switch component.style { + case .default: + backgroundColor = .color(environment.theme.list.modalPlainBackgroundColor) + case .media: + backgroundColor = .color(UIColor(rgb: 0x1c1c1e)) + } + + let _ = self.sheet.update( + transition: transition, + component: AnyComponent( + SheetComponent( + content: AnyComponent( + LocationDistancePickerContentComponent( + style: component.style, + compactDisplayTitle: component.compactDisplayTitle, + distances: component.distances, + updated: component.updated, + completion: { [weak self] distance in + guard let self, let component = self.component else { + return + } + component.completion(distance, { [weak self] in + self?.requestDismiss() + }) + }, + dismiss: { [weak self] in + self?.requestDismiss() + } + ) + ), + style: .glass, + backgroundColor: backgroundColor, + hasDimView: false, + animateOut: self.sheetAnimateOut, + willDismiss: { [weak self] in + self?.performWillDismissOnce() + } + ) + ), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + return availableSize + } } - private func updateDoneButtonTitle() { - if let pickerView = self.pickerView { + 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 LocationDistancePickerContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let style: LocationDistancePickerScreenStyle + let compactDisplayTitle: String? + let distances: Signal<[Double], NoError> + let updated: (Int32) -> Void + let completion: (Int32) -> Void + let dismiss: () -> Void + + init( + style: LocationDistancePickerScreenStyle, + compactDisplayTitle: String?, + distances: Signal<[Double], NoError>, + updated: @escaping (Int32) -> Void, + completion: @escaping (Int32) -> Void, + dismiss: @escaping () -> Void + ) { + self.style = style + self.compactDisplayTitle = compactDisplayTitle + self.distances = distances + self.updated = updated + self.completion = completion + self.dismiss = dismiss + } + + static func ==(lhs: LocationDistancePickerContentComponent, rhs: LocationDistancePickerContentComponent) -> Bool { + if lhs.style != rhs.style { + return false + } + if lhs.compactDisplayTitle != rhs.compactDisplayTitle { + return false + } + return true + } + + final class View: UIView, UIPickerViewDataSource, UIPickerViewDelegate { + private let closeButton = ComponentView() + private let title = ComponentView() + private let unitLabel = ComponentView() + private let smallUnitLabel = ComponentView() + private let button = ComponentView() + private let warningText = ComponentView() + + private var pickerView: TimerPickerView? + private var pickerTimer: SwiftSignalKit.Timer? + private var distancesDisposable: Disposable? + + private var component: LocationDistancePickerContentComponent? + private weak var state: EmptyComponentState? + private var environment: EnvironmentType? + private var distances: [Double] = [] + private var previousReportedValue: Int32? + private var isCompleting = false + + private var isUpdating = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.distancesDisposable?.dispose() + self.pickerTimer?.invalidate() + } + + private var usesMetricSystem: Bool { + guard let environment = self.environment else { + return true + } + let locale = localeWithStrings(environment.strings) + if locale.identifier.hasSuffix("GB") { + return false + } + return locale.usesMetricSystem + } + + private func setupDistancesIfNeeded(component: LocationDistancePickerContentComponent) { + if self.distancesDisposable != nil { + return + } + self.distancesDisposable = (component.distances + |> deliverOnMainQueue).start(next: { [weak self] distances in + guard let self else { + return + } + self.distances = distances + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + }) + } + + private func setupPickerViewIfNeeded() { + if self.pickerView != nil { + return + } + + let pickerView = TimerPickerView() + pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) + pickerView.dataSource = self + pickerView.delegate = self + pickerView.selectRow(0, inComponent: 0, animated: false) + if self.usesMetricSystem { + pickerView.selectRow(6, inComponent: 1, animated: false) + } else { + pickerView.selectRow(4, inComponent: 1, animated: false) + } + self.addSubview(pickerView) + self.pickerView = pickerView + + let pickerTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: true, completion: { [weak self] in + guard let self else { + return + } + if self.reportSelectedValue() { + self.state?.updated(transition: .immediate) + } + }, queue: Queue.mainQueue()) + self.pickerTimer = pickerTimer + pickerTimer.start() + + let _ = self.reportSelectedValue() + } + + private func selectedDistance() -> (value: Int32, convertedValue: Int32, convertedDistance: Double, distanceText: String)? { + guard let pickerView = self.pickerView, let environment = self.environment else { + return nil + } + let selectedLargeRow = pickerView.selectedRow(inComponent: 0) var selectedSmallRow = pickerView.selectedRow(inComponent: 1) if selectedLargeRow == 0 && selectedSmallRow == 0 { @@ -390,275 +456,309 @@ class LocationDistancePickerScreenNode: ViewControllerTracingNode, ASScrollViewD if smallValue == 5 { formattedValue = formattedValue.replacingOccurrences(of: ".1", with: ".05").replacingOccurrences(of: ",1", with: ",05") } - let distance = self.usesMetricSystem ? "\(formattedValue) \(self.presentationData.strings.Location_ProximityNotification_DistanceKM)" : "\(formattedValue) \(self.presentationData.strings.Location_ProximityNotification_DistanceMI)" + let distanceText = self.usesMetricSystem ? "\(formattedValue) \(environment.strings.Location_ProximityNotification_DistanceKM)" : "\(formattedValue) \(environment.strings.Location_ProximityNotification_DistanceMI)" - let shortTitle = self.presentationData.strings.Location_ProximityNotification_Notify(distance).string - var longTitle: String? - if let displayTitle = self.compactDisplayTitle, let (layout, _) = self.containerLayout { - let title = self.presentationData.strings.Location_ProximityNotification_NotifyLong(displayTitle, distance).string - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - self.measureButtonTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: .black) - let titleSize = self.measureButtonTitleNode.updateLayout(CGSize(width: width * 2.0, height: 50.0)) - if titleSize.width < width - 70.0 { - longTitle = title + var convertedDistance = Double(value) + if !self.usesMetricSystem { + convertedDistance = convertedDistance * 1.60934 + } + + return (value, Int32(convertedDistance), convertedDistance, distanceText) + } + + private func reportSelectedValue() -> Bool { + guard let selectedDistance = self.selectedDistance(), let component = self.component else { + return false + } + if let previousReportedValue = self.previousReportedValue, selectedDistance.convertedValue == previousReportedValue { + return false + } + self.previousReportedValue = selectedDistance.convertedValue + component.updated(selectedDistance.convertedValue) + return true + } + + func update(component: LocationDistancePickerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let previousEnvironment = self.environment + self.component = component + self.state = state + + let environment = environment[EnvironmentType.self].value + self.environment = environment + + self.setupDistancesIfNeeded(component: component) + self.setupPickerViewIfNeeded() + + if let previousEnvironment, previousEnvironment.strings !== environment.strings || previousEnvironment.theme !== environment.theme { + self.pickerView?.reloadAllComponents() + } + + let textColor: UIColor + let secondaryTextColor: UIColor + let buttonFillColor: UIColor + let buttonForegroundColor: UIColor + switch component.style { + case .default: + textColor = environment.theme.actionSheet.primaryTextColor + secondaryTextColor = environment.theme.actionSheet.secondaryTextColor + buttonFillColor = environment.theme.list.itemCheckColors.fillColor + buttonForegroundColor = environment.theme.list.itemCheckColors.foregroundColor + case .media: + textColor = .white + secondaryTextColor = UIColor(white: 1.0, alpha: 0.7) + buttonFillColor = environment.theme.list.itemCheckColors.fillColor + buttonForegroundColor = environment.theme.list.itemCheckColors.foregroundColor + } + + let sideInset: CGFloat = 16.0 + let topInset: CGFloat = 16.0 + let titleHeight: CGFloat = 54.0 + let pickerHeight: CGFloat = 216.0 + let buttonHeight: CGFloat = 52.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: buttonHeight, sideInset: 30.0) + let buttonWidth = availableSize.width - buttonInsets.left - buttonInsets.right + + let selectedDistance = self.selectedDistance() + let distanceText = selectedDistance?.distanceText ?? "" + let isTooFar: Bool + if let selectedDistance, let maximumDistance = self.distances.last { + isTooFar = selectedDistance.convertedDistance > maximumDistance + } else { + isTooFar = false + } + + let closeButtonSize = CGSize(width: 44.0, height: 44.0) + let closeSize = self.closeButton.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: closeButtonSize, + backgroundColor: nil, + isDark: component.style == .media ? true : environment.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak self] _ in + self?.component?.dismiss() + } + ) + ), + environment: {}, + containerSize: closeButtonSize + ) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: sideInset, y: topInset), size: closeSize)) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(Text( + text: environment.strings.Location_ProximityNotification_Title, + font: Font.bold(17.0), + color: textColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 120.0, height: titleHeight) + ) + 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) * 0.5), y: topInset + floorToScreenPixels((closeButtonSize.height - titleSize.height) * 0.5)), size: titleSize)) + } + + let pickerFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + titleHeight), size: CGSize(width: availableSize.width, height: pickerHeight)) + if let pickerView = self.pickerView { + transition.setFrame(view: pickerView, frame: pickerFrame) + } + + let unitLabelSize = self.unitLabel.update( + transition: transition, + component: AnyComponent(Text( + text: self.usesMetricSystem ? environment.strings.Location_ProximityNotification_DistanceKM : environment.strings.Location_ProximityNotification_DistanceMI, + font: Font.regular(15.0), + color: textColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: titleHeight) + ) + if let unitLabelView = self.unitLabel.view { + if unitLabelView.superview == nil { + self.addSubview(unitLabelView) + } + transition.setFrame(view: unitLabelView, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0) + 50.0, y: floor(pickerFrame.midY - unitLabelSize.height / 2.0)), size: unitLabelSize)) + } + + let smallUnitLabelSize = self.smallUnitLabel.update( + transition: transition, + component: AnyComponent(Text( + text: self.usesMetricSystem ? environment.strings.Location_ProximityNotification_DistanceM : "", + font: Font.regular(15.0), + color: textColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: titleHeight) + ) + if let smallUnitLabelView = self.smallUnitLabel.view { + if smallUnitLabelView.superview == nil { + self.addSubview(smallUnitLabelView) + } + transition.setFrame(view: smallUnitLabelView, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0 * 3.0) + 50.0, y: floor(pickerFrame.midY - smallUnitLabelSize.height / 2.0)), size: smallUnitLabelSize)) + } + + let bottomY = pickerFrame.maxY + 17.0 + var buttonTitle = environment.strings.Location_ProximityNotification_Notify(distanceText).string + if let displayTitle = component.compactDisplayTitle { + let longTitle = environment.strings.Location_ProximityNotification_NotifyLong(displayTitle, distanceText).string + let titleSize = NSAttributedString(string: longTitle, font: Font.semibold(17.0), textColor: .black).boundingRect(with: CGSize(width: availableSize.width * 2.0, height: 50.0), options: .usesLineFragmentOrigin, context: nil).size + if titleSize.width < availableSize.width - 70.0 { + buttonTitle = longTitle } } - self.doneButton.title = longTitle ?? shortTitle - - self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Location_ProximityNotification_AlreadyClose(distance).string, font: Font.regular(14.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor) - if let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + + var buttonTransition = transition + if transition.animation.isImmediate { + buttonTransition = buttonTransition.withAnimation(.curve(duration: 0.2, curve: .easeInOut)) + } + let buttonSize = self.button.update( + transition: buttonTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: buttonFillColor, + foreground: buttonForegroundColor, + pressedColor: buttonFillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable("title"), component: AnyComponent( + AnimatedTextComponent( + font: Font.semibold(17.0), + color: buttonForegroundColor, + items: [ + AnimatedTextComponent.Item(id: AnyHashable("title"), content: .text(buttonTitle)) + ], + noDelay: true + ) + )), + isEnabled: !isTooFar && !self.isCompleting, + tintWhenDisabled: false, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component, let selectedDistance = self.selectedDistance() else { + return + } + self.isCompleting = true + self.state?.updated(transition: .immediate) + component.completion(selectedDistance.convertedValue) + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: buttonHeight) + ) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: bottomY), size: buttonSize)) + buttonTransition.setAlpha(view: buttonView, alpha: isTooFar ? 0.0 : 1.0) } - var convertedValue = Double(value) - if !self.usesMetricSystem { - convertedValue = Double(convertedValue) * 1.60934 + let warningSize = self.warningText.update( + transition: transition, + component: AnyComponent(Text( + text: environment.strings.Location_ProximityNotification_AlreadyClose(distanceText).string, + font: Font.regular(14.0), + color: secondaryTextColor + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) + ) + if let warningTextView = self.warningText.view { + if warningTextView.superview == nil { + self.addSubview(warningTextView) + } + transition.setFrame(view: warningTextView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - warningSize.width) * 0.5), y: bottomY + floorToScreenPixels((buttonHeight - warningSize.height) * 0.5)), size: warningSize)) + buttonTransition.setAlpha(view: warningTextView, alpha: isTooFar ? 1.0 : 0.0) } - if let distance = self.distances.last, convertedValue > distance { - self.doneButton.alpha = 0.0 - self.doneButton.isUserInteractionEnabled = false - self.textNode.alpha = 1.0 + return CGSize(width: availableSize.width, height: bottomY + buttonHeight + buttonInsets.bottom) + } + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 2 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + if component == 0 { + return unitValues.count + } else if component == 1 { + return smallUnitValues.count } else { - self.doneButton.alpha = 1.0 - self.doneButton.isUserInteractionEnabled = true - self.textNode.alpha = 0.0 + return 1 } } - } - - var previousReportedValue: Int32? - fileprivate func update() -> Bool { - if let pickerView = self.pickerView { - let selectedLargeRow = pickerView.selectedRow(inComponent: 0) - var selectedSmallRow = pickerView.selectedRow(inComponent: 1) - if selectedLargeRow == 0 && selectedSmallRow == 0 { - selectedSmallRow = 1 + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + guard let environment = self.environment else { + return nil } - let largeValue = unitValues[selectedLargeRow] - let smallValue = smallUnitValues[selectedSmallRow] - - var value = largeValue * 1000 + smallValue * 10 - if !self.usesMetricSystem { - value = Int32(Double(value) * 1.60934) - } - - if let previousReportedValue = self.previousReportedValue, value == previousReportedValue { - return false - } else { - self.updated?(value) - self.previousReportedValue = value - return true - } - } else { - return false - } - } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - if pickerView.selectedRow(inComponent: 0) == 0 && pickerView.selectedRow(inComponent: 1) == 0 { - pickerView.selectRow(1, inComponent: 1, animated: true) - } - self.updateDoneButtonTitle() - let _ = self.update() - } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - if component == 0 { - return unitValues.count - } else if component == 1 { - return smallUnitValues.count - } else { - return 1 - } - } - - func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { - let font = Font.regular(17.0) - let string: String - if component == 0 { - let value = unitValues[row] - string = "\(value)" - } else { - if self.usesMetricSystem { - let value = String(format: "%d", smallUnitValues[row] * 10) + let font = Font.regular(17.0) + let string: String + if component == 0 { + let value = unitValues[row] string = "\(value)" } else { - let value = smallUnitValues[row] - if value == 0 { - string = ".0" - } else if value == 5 { - string = ".05" + if self.usesMetricSystem { + let value = String(format: "%d", smallUnitValues[row] * 10) + string = "\(value)" } else { - string = ".\(value / 10)" + let value = smallUnitValues[row] + if value == 0 { + string = ".0" + } else if value == 5 { + string = ".05" + } else { + string = ".\(value / 10)" + } } } - } - return NSAttributedString(string: string, font: font, textColor: self.presentationData.theme.actionSheet.primaryTextColor) - } - - func updatePresentationData(_ presentationData: PresentationData) { - let previousTheme = self.presentationData.theme - self.presentationData = presentationData - - guard case .default = self.controllerStyle else { - return - } - - if let effectView = self.effectNode.view as? UIVisualEffectView { - effectView.effect = UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } - - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - - if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { - self.setupPickerView() - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) - self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) - - self.updateDoneButtonTitle() - - self.unitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceKM : self.presentationData.strings.Location_ProximityNotification_DistanceMI, font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - self.smallUnitLabelNode.attributedText = NSAttributedString(string: self.usesMetricSystem ? self.presentationData.strings.Location_ProximityNotification_DistanceM : "", font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - } - - override func didLoad() { - super.didLoad() - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never - } - } - - @objc func cancelButtonPressed() { - self.cancel?() - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.cancelButtonPressed() - } - } - - func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - - let offset = self.contentContainerNode.frame.height - let position = self.wrappingScrollNode.position - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - self.wrappingScrollNode.position = CGPoint(x: position.x, y: position.y + offset) - transition.animateView({ - self.wrappingScrollNode.position = position - }) - } - - func animateOut(completion: (() -> Void)? = nil) { - var dimCompleted = false - var offsetCompleted = false - - let internalCompletion: () -> Void = { [weak self] in - if let strongSelf = self, dimCompleted && offsetCompleted { - strongSelf.dismiss?() + + let textColor: UIColor + switch self.component?.style { + case .media: + textColor = .white + default: + textColor = environment.theme.actionSheet.primaryTextColor } - completion?() + return NSAttributedString(string: string, font: font, textColor: textColor) } - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - dimCompleted = true - internalCompletion() - }) - - let offset = self.contentContainerNode.frame.height - self.wrappingScrollNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in - offsetCompleted = true - internalCompletion() - }) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { - return self.dimNode.view + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + if pickerView.selectedRow(inComponent: 0) == 0 && pickerView.selectedRow(inComponent: 1) == 0 { + pickerView.selectRow(1, inComponent: 1, animated: true) } - } - return super.hitTest(point, with: event) - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let contentOffset = scrollView.contentOffset - let additionalTopHeight = max(0.0, -contentOffset.y) - - if additionalTopHeight >= 30.0 { - self.cancelButtonPressed() + let _ = self.reportSelectedValue() + self.state?.updated(transition: .immediate) } } - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let hadValidLayout = self.containerLayout != nil - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.statusBar, .input]) - let cleanInsets = layout.insets(options: [.statusBar]) - insets.top = max(10.0, insets.top) - - let buttonOffset: CGFloat = 0.0 - let bottomInset: CGFloat = 10.0 + cleanInsets.bottom - let titleHeight: CGFloat = 54.0 - var contentHeight = titleHeight + bottomInset + 52.0 + 17.0 - let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight) - contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + pickerHeight + buttonOffset - - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - let sideInset = floor((layout.size.width - width) / 2.0) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) - let contentFrame = contentContainerFrame - - var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) - if backgroundFrame.minY < contentFrame.minY { - backgroundFrame.origin.y = contentFrame.minY - } - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: insets.top + 66.0 + UIScreenPixel))) - - let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight)) - let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize) - transition.updateFrame(node: self.cancelButton, frame: cancelFrame) - - let buttonInset: CGFloat = 16.0 - let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - let doneButtonFrame = CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight) - transition.updateFrame(node: self.doneButton, frame: doneButtonFrame) - - let textSize = self.textNode.updateLayout(CGSize(width: width, height: titleHeight)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: floor(doneButtonFrame.center.y - textSize.height / 2.0)), size: textSize)) - - let pickerFrame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight)) - self.pickerView?.frame = pickerFrame - - let unitLabelSize = self.unitLabelNode.updateLayout(CGSize(width: width, height: titleHeight)) - transition.updateFrame(node: self.unitLabelNode, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0) + 50.0, y: floor(pickerFrame.center.y - unitLabelSize.height / 2.0)), size: unitLabelSize)) - - let smallUnitLabelSize = self.smallUnitLabelNode.updateLayout(CGSize(width: width, height: titleHeight)) - transition.updateFrame(node: self.smallUnitLabelNode, frame: CGRect(origin: CGPoint(x: floor(pickerFrame.width / 4.0 * 3.0) + 50.0, y: floor(pickerFrame.center.y - smallUnitLabelSize.height / 2.0)), size: smallUnitLabelSize)) - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - - if !hadValidLayout { - self.updateDoneButtonTitle() - } + 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) } } diff --git a/submodules/LocationUI/Sources/LocationInfoListItem.swift b/submodules/LocationUI/Sources/LocationInfoListItem.swift index 032f1f621c..d87f8c99a1 100644 --- a/submodules/LocationUI/Sources/LocationInfoListItem.swift +++ b/submodules/LocationUI/Sources/LocationInfoListItem.swift @@ -7,9 +7,10 @@ import TelegramCore import TelegramPresentationData import ItemListUI import LocationResources -import AppBundle -import SolidRoundedButtonNode import ShimmerEffect +import ComponentFlow +import ButtonComponent +import BundleIconComponent public final class LocationInfoListItem: ListViewItem { let presentationData: ItemListPresentationData @@ -18,27 +19,35 @@ public final class LocationInfoListItem: ListViewItem { let address: String? let distance: String? let drivingTime: ExpectedTravelTime - let transitTime: ExpectedTravelTime let walkingTime: ExpectedTravelTime let hasEta: Bool let action: () -> Void let drivingAction: () -> Void - let transitAction: () -> Void let walkingAction: () -> Void - public init(presentationData: ItemListPresentationData, engine: TelegramEngine, location: TelegramMediaMap, address: String?, distance: String?, drivingTime: ExpectedTravelTime, transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, hasEta: Bool, action: @escaping () -> Void, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) { + public init( + presentationData: ItemListPresentationData, + engine: TelegramEngine, + location: TelegramMediaMap, + address: String?, + distance: String?, + drivingTime: ExpectedTravelTime, + walkingTime: ExpectedTravelTime, + hasEta: Bool, + action: @escaping () -> Void, + drivingAction: @escaping () -> Void, + walkingAction: @escaping () -> Void + ) { self.presentationData = presentationData self.engine = engine self.location = location self.address = address self.distance = distance self.drivingTime = drivingTime - self.transitTime = transitTime self.walkingTime = walkingTime self.hasEta = hasEta self.action = action self.drivingAction = drivingAction - self.transitAction = transitAction self.walkingAction = walkingAction } @@ -76,31 +85,26 @@ public final class LocationInfoListItem: ListViewItem { } public final class LocationInfoListItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode private var titleNode: TextNode? private var subtitleNode: TextNode? private let venueIconNode: TransformImageNode private let buttonNode: HighlightableButtonNode private var placeholderNode: ShimmerEffectNode? - private var drivingButtonNode: SolidRoundedButtonNode? - private var transitButtonNode: SolidRoundedButtonNode? - private var walkingButtonNode: SolidRoundedButtonNode? + private let drivingButton = ComponentView() + private let walkingButton = ComponentView() private var item: LocationInfoListItem? private var layoutParams: ListViewItemLayoutParams? private var absoluteLocation: (CGRect, CGSize)? required public init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true self.buttonNode = HighlightableButtonNode() self.venueIconNode = TransformImageNode() self.venueIconNode.isUserInteractionEnabled = false super.init(layerBacked: false, rotated: false, seeThrough: false) - //self.addSubnode(self.backgroundNode) self.addSubnode(self.buttonNode) self.addSubnode(self.venueIconNode) @@ -145,14 +149,15 @@ public final class LocationInfoListItemNode: ListViewItemNode { let iconLayout = self.venueIconNode.asyncLayout() return { [weak self] item, params in - let leftInset: CGFloat = 75.0 + params.leftInset + let leftInset: CGFloat = 78.0 + params.leftInset let rightInset: CGFloat = params.rightInset let verticalInset: CGFloat = 14.0 - let iconSize: CGFloat = 48.0 - let inset: CGFloat = 15.0 + let iconSize: CGFloat = 40.0 + let directionsButtonHeight: CGFloat = 52.0 + let directionsTopInset: CGFloat = 18.0 - let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) - let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + let titleFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize) + let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) let title: String let subtitle: String @@ -179,10 +184,11 @@ public final class LocationInfoListItemNode: ListViewItemNode { let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let titleSpacing: CGFloat = 1.0 - let bottomInset: CGFloat = 4.0 + let titleSpacing: CGFloat = 0.0 + let bottomInset: CGFloat = 16.0 let textContentSize = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset - let contentSize = CGSize(width: params.width, height: item.hasEta ? max(100.0, textContentSize) : textContentSize) + let etaContentSize = verticalInset + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + directionsTopInset + directionsButtonHeight + bottomInset + let contentSize = CGSize(width: params.width, height: item.hasEta ? max(etaContentSize, textContentSize) : textContentSize) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) return (nodeLayout, { [weak self] in @@ -200,13 +206,7 @@ public final class LocationInfoListItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item strongSelf.layoutParams = params - - if let _ = updatedTheme { - strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor - } - - strongSelf.backgroundNode.isHidden = params.isStandalone - + let arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor) if let updatedLocation = updatedLocation { strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: updatedLocation.venue?.type ?? "", background: true)) @@ -229,107 +229,126 @@ public final class LocationInfoListItemNode: ListViewItemNode { strongSelf.addSubnode(subtitleNode) } - let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) - if strongSelf.drivingButtonNode == nil { - strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.drivingButtonNode?.iconSpacing = 5.0 - strongSelf.drivingButtonNode?.alpha = 0.0 - strongSelf.drivingButtonNode?.allowsGroupOpacity = true - strongSelf.drivingButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.drivingAction() - } - } - strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) } - - strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.transitButtonNode?.iconSpacing = 2.0 - strongSelf.transitButtonNode?.alpha = 0.0 - strongSelf.transitButtonNode?.allowsGroupOpacity = true - strongSelf.transitButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.transitAction() - } - } - strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) } - - strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.walkingButtonNode?.iconSpacing = 2.0 - strongSelf.walkingButtonNode?.alpha = 0.0 - strongSelf.walkingButtonNode?.allowsGroupOpacity = true - strongSelf.walkingButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.walkingAction() - } - } - strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) } - } else if let _ = updatedTheme { - strongSelf.drivingButtonNode?.updateTheme(buttonTheme) - strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - - strongSelf.transitButtonNode?.updateTheme(buttonTheme) - strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - - strongSelf.walkingButtonNode?.updateTheme(buttonTheme) - strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) titleNode.frame = titleFrame let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size) subtitleNode.frame = subtitleFrame - let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + inset, y: 10.0), size: CGSize(width: iconSize, height: iconSize)) + let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 26.0, y: 14.0), size: CGSize(width: iconSize, height: iconSize)) strongSelf.venueIconNode.frame = iconNodeFrame - var directionsWidth: CGFloat = 93.0 + let glassInset: CGFloat = 6.0 + let buttonSideInset: CGFloat = 30.0 + let buttonSpacing: CGFloat = 10.0 + var directionsWidth: CGFloat = floorToScreenPixels((params.width - glassInset * 2.0 - buttonSideInset * 2.0 - buttonSpacing) / 2.0) if item.hasEta { - if item.drivingTime == .unknown && item.transitTime == .unknown && item.walkingTime == .unknown { - strongSelf.drivingButtonNode?.icon = nil - strongSelf.drivingButtonNode?.title = item.presentationData.strings.Map_GetDirections - if let drivingButtonNode = strongSelf.drivingButtonNode { - let buttonSize = drivingButtonNode.sizeThatFits(contentSize) - directionsWidth = buttonSize.width - } + let buttonBackground = ButtonComponent.Background( + style: .glass, + color: item.presentationData.theme.list.itemCheckColors.fillColor, + foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ) + let foregroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor + + var drivingButtonTitle = "" + var walkingButtonTitle = "" + var drivingButtonHasIcon = true + var drivingButtonVisible = false + var walkingButtonVisible = false + + if item.drivingTime == .unknown && item.walkingTime == .unknown { + drivingButtonHasIcon = false + drivingButtonTitle = item.presentationData.strings.Map_GetDirections + drivingButtonVisible = true if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime { - strongSelf.drivingButtonNode?.alpha = 1.0 - strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.drivingButton.view?.alpha = 1.0 + strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } else { if case let .ready(drivingTime) = item.drivingTime { - strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) + drivingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) ?? "" + drivingButtonVisible = true if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime { - strongSelf.drivingButtonNode?.alpha = 1.0 - strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - - if case let .ready(transitTime) = item.transitTime { - strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 }) - - if let previousTransitTime = currentItem?.transitTime, case .calculating = previousTransitTime { - strongSelf.transitButtonNode?.alpha = 1.0 - strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.drivingButton.view?.alpha = 1.0 + strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } if case let .ready(walkingTime) = item.walkingTime { - strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) + walkingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) ?? "" + walkingButtonVisible = true if let previousWalkingTime = currentItem?.walkingTime, case .calculating = previousWalkingTime { - strongSelf.walkingButtonNode?.alpha = 1.0 - strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.walkingButton.view?.alpha = 1.0 + strongSelf.walkingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } } + + let drivingButtonContent: AnyComponent + if drivingButtonHasIcon { + drivingButtonContent = AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsDriving", tintColor: foregroundColor))), + AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(17.0), color: foregroundColor))) + ], spacing: 5.0) + ) + } else { + drivingButtonContent = AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(17.0), color: foregroundColor)) + } - let directionsSpacing: CGFloat = 8.0 + let drivingButtonSize = strongSelf.drivingButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("driving-\(drivingButtonHasIcon)-\(drivingButtonTitle)"), + component: drivingButtonContent + ), + action: { [weak self] in + if let item = self?.item { + item.drivingAction() + } + } + )), + environment: {}, + containerSize: CGSize(width: drivingButtonHasIcon ? directionsWidth : contentSize.width - glassInset * 2.0 - buttonSideInset * 2.0, height: directionsButtonHeight) + ) + if !drivingButtonHasIcon { + directionsWidth = drivingButtonSize.width + } - if case .calculating = item.drivingTime, case .calculating = item.transitTime, case .calculating = item.walkingTime { + let walkingButtonSize = strongSelf.walkingButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("walking-\(walkingButtonTitle)"), + component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsWalking", tintColor: foregroundColor))), + AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: walkingButtonTitle, font: Font.semibold(17.0), color: foregroundColor))) + ], spacing: 2.0) + ) + ), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + if let item = self?.item { + item.walkingAction() + } + } + )), + environment: {}, + containerSize: CGSize(width: directionsWidth, height: directionsButtonHeight) + ) + + var buttonOrigin = glassInset + buttonSideInset + + if case .calculating = item.drivingTime, case .calculating = item.walkingTime { let shimmerNode: ShimmerEffectNode if let current = strongSelf.placeholderNode { shimmerNode = current @@ -338,17 +357,17 @@ public final class LocationInfoListItemNode: ListViewItemNode { strongSelf.placeholderNode = shimmerNode strongSelf.addSubnode(shimmerNode) } - shimmerNode.frame = CGRect(origin: CGPoint(x: leftInset, y: subtitleFrame.maxY + 12.0), size: CGSize(width: contentSize.width - leftInset, height: 32.0)) + shimmerNode.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: contentSize.width - buttonOrigin * 2.0, height: directionsButtonHeight)) if let (rect, size) = strongSelf.absoluteLocation { shimmerNode.updateAbsoluteRect(rect, within: size) } var shapes: [ShimmerEffectNode.Shape] = [] - shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: directionsWidth, diameter: 32.0)) - shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + directionsSpacing, y: 0.0), width: directionsWidth, diameter: 32.0)) - shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + directionsSpacing + directionsWidth + directionsSpacing, y: 0.0), width: directionsWidth, diameter: 32.0)) + shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight)) + shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + buttonSpacing, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight)) + shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + buttonSpacing + directionsWidth + buttonSpacing, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight)) - shimmerNode.update(backgroundColor: item.presentationData.theme.list.plainBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerNode.frame.size) + shimmerNode.update(backgroundColor: .clear, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerNode.frame.size, mask: true) } else if let shimmerNode = strongSelf.placeholderNode { strongSelf.placeholderNode = nil shimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak shimmerNode] _ in @@ -356,30 +375,42 @@ public final class LocationInfoListItemNode: ListViewItemNode { }) } - let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - var buttonOrigin = leftInset - strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight)) + let drivingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: directionsWidth, height: drivingButtonSize.height)) + if let drivingButtonView = strongSelf.drivingButton.view { + if drivingButtonView.superview == nil { + strongSelf.view.addSubview(drivingButtonView) + } + drivingButtonView.frame = drivingButtonFrame + if drivingButtonView.layer.animation(forKey: "opacity") == nil { + drivingButtonView.alpha = drivingButtonVisible ? 1.0 : 0.0 + } + } if case .ready = item.drivingTime { - buttonOrigin += directionsWidth + directionsSpacing + buttonOrigin += directionsWidth + buttonSpacing } - strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight)) - - if case .ready = item.transitTime { - buttonOrigin += directionsWidth + directionsSpacing + let walkingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: directionsWidth, height: walkingButtonSize.height)) + if let walkingButtonView = strongSelf.walkingButton.view { + if walkingButtonView.superview == nil { + strongSelf.view.addSubview(walkingButtonView) + } + walkingButtonView.frame = walkingButtonFrame + if walkingButtonView.layer.animation(forKey: "opacity") == nil { + walkingButtonView.alpha = walkingButtonVisible ? 1.0 : 0.0 + } } - - strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight)) } else { - + strongSelf.drivingButton.view?.alpha = 0.0 + strongSelf.walkingButton.view?.alpha = 0.0 + if let shimmerNode = strongSelf.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } } strongSelf.buttonNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: 72.0) - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) } }) }) diff --git a/submodules/LocationUI/Sources/LocationLiveListItem.swift b/submodules/LocationUI/Sources/LocationLiveListItem.swift index 5aaaf511fc..be96523c7c 100644 --- a/submodules/LocationUI/Sources/LocationLiveListItem.swift +++ b/submodules/LocationUI/Sources/LocationLiveListItem.swift @@ -10,10 +10,11 @@ import TelegramUIPreferences import TelegramStringFormatting import ItemListUI import LocationResources -import AppBundle import AvatarNode import LiveLocationTimerNode -import SolidRoundedButtonNode +import ComponentFlow +import ButtonComponent +import BundleIconComponent final class LocationLiveListItem: ListViewItem { let presentationData: ItemListPresentationData @@ -24,17 +25,15 @@ final class LocationLiveListItem: ListViewItem { let distance: Double? let drivingTime: ExpectedTravelTime - let transitTime: ExpectedTravelTime let walkingTime: ExpectedTravelTime let action: () -> Void let longTapAction: () -> Void let drivingAction: () -> Void - let transitAction: () -> Void let walkingAction: () -> Void - public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: EngineMessage, distance: Double?, drivingTime: ExpectedTravelTime, transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: EngineMessage, distance: Double?, drivingTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, walkingAction: @escaping () -> Void) { self.presentationData = presentationData self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder @@ -42,12 +41,10 @@ final class LocationLiveListItem: ListViewItem { self.message = message self.distance = distance self.drivingTime = drivingTime - self.transitTime = transitTime self.walkingTime = walkingTime self.action = action self.longTapAction = longTapAction self.drivingAction = drivingAction - self.transitAction = transitAction self.walkingAction = walkingAction } @@ -91,28 +88,19 @@ final class LocationLiveListItem: ListViewItem { private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)) final class LocationLiveListItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode private var titleNode: TextNode? private var subtitleNode: TextNode? private let avatarNode: AvatarNode private var timerNode: ChatMessageLiveLocationTimerNode? - private var drivingButtonNode: SolidRoundedButtonNode? - private var transitButtonNode: SolidRoundedButtonNode? - private var walkingButtonNode: SolidRoundedButtonNode? + private let drivingButton = ComponentView() + private let walkingButton = ComponentView() private var item: LocationLiveListItem? private var layoutParams: ListViewItemLayoutParams? required init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true @@ -121,8 +109,6 @@ final class LocationLiveListItemNode: ListViewItemNode { super.init(layerBacked: false, rotated: false, seeThrough: false) - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) self.addSubnode(self.avatarNode) } @@ -142,7 +128,7 @@ final class LocationLiveListItemNode: ListViewItemNode { if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) + self.insertSubnode(self.highlightedBackgroundNode, at: 0) } } else { if self.highlightedBackgroundNode.supernode != nil { @@ -169,7 +155,7 @@ final class LocationLiveListItemNode: ListViewItemNode { let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) return { [weak self] item, params, hasSeparator in - let leftInset: CGFloat = 65.0 + params.leftInset + let leftInset: CGFloat = 72.0 + params.leftInset let rightInset: CGFloat = params.rightInset let verticalInset: CGFloat = 8.0 @@ -203,18 +189,17 @@ final class LocationLiveListItemNode: ListViewItemNode { let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 54.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let titleSpacing: CGFloat = 1.0 + let titleSpacing: CGFloat = 0.0 var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height) - let hasEta: Bool + var hasEta: Bool if case .ready = item.drivingTime { hasEta = true - } else if case .ready = item.transitTime { - hasEta = true } else if case .ready = item.walkingTime { hasEta = true } else { hasEta = false } + hasEta = true if hasEta { contentSize.height += 46.0 } @@ -232,9 +217,7 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.layoutParams = params if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.contextMenu.itemHighlightedBackgroundColor } let titleNode = titleApply() @@ -249,72 +232,23 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.addSubnode(subtitleNode) } - let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme) - if strongSelf.drivingButtonNode == nil { - strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.drivingButtonNode?.alpha = 0.0 - strongSelf.drivingButtonNode?.iconSpacing = 5.0 - strongSelf.drivingButtonNode?.allowsGroupOpacity = true - strongSelf.drivingButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.drivingAction() - } - } - strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) } - - strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.transitButtonNode?.alpha = 0.0 - strongSelf.transitButtonNode?.iconSpacing = 2.0 - strongSelf.transitButtonNode?.allowsGroupOpacity = true - strongSelf.transitButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.transitAction() - } - } - strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) } - - strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0) - strongSelf.walkingButtonNode?.alpha = 0.0 - strongSelf.walkingButtonNode?.iconSpacing = 2.0 - strongSelf.walkingButtonNode?.allowsGroupOpacity = true - strongSelf.walkingButtonNode?.pressed = { [weak self] in - if let item = self?.item { - item.walkingAction() - } - } - strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) } - } else if let _ = updatedTheme { - strongSelf.drivingButtonNode?.updateTheme(buttonTheme) - strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - - strongSelf.transitButtonNode?.updateTheme(buttonTheme) - strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - - strongSelf.walkingButtonNode?.updateTheme(buttonTheme) - strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) - } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) titleNode.frame = titleFrame let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size) subtitleNode.frame = subtitleFrame - let separatorHeight = UIScreenPixel - let topHighlightInset: CGFloat = separatorHeight - let separatorRightInset: CGFloat = 16.0 let avatarSize: CGFloat = 40.0 - if let peer = item.message.author { strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: nil, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: false) } - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize)) + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 22.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize)) - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - leftInset - params.rightInset - separatorRightInset, height: separatorHeight)) - strongSelf.separatorNode.isHidden = !hasSeparator + let highlightFrame = CGRect(origin: CGPoint(x: 14.0, y: 2.0), size: CGSize(width: contentSize.width - 14.0 * 2.0, height: 52.0)) + let highlightCornerRadius = highlightFrame.height * 0.5 + strongSelf.highlightedBackgroundNode.frame = highlightFrame + strongSelf.highlightedBackgroundNode.cornerRadius = highlightCornerRadius var liveBroadcastingTimeout: Int32 = 0 if let location = getLocation(from: item.message), let timeout = location.liveBroadcastingTimeout { @@ -338,61 +272,134 @@ final class LocationLiveListItemNode: ListViewItemNode { strongSelf.addSubnode(timerNode) strongSelf.timerNode = timerNode } - let timerSize = CGSize(width: 28.0, height: 28.0) + let timerSize = CGSize(width: 24.0, height: 24.0) timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Int32(liveBroadcastingTimeout) == liveLocationIndefinitePeriod ? -1.0 : Double(liveBroadcastingTimeout), strings: item.presentationData.strings) - timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: 14.0), size: timerSize) + timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 26.0 - timerSize.width, y: floorToScreenPixels((56.0 - timerSize.height) / 2.0)), size: timerSize) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil timerNode.removeFromSupernode() } + let buttonBackground = ButtonComponent.Background( + style: .glass, + color: item.presentationData.theme.list.itemCheckColors.fillColor, + foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ) + let foregroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor + var directionsSize = CGSize(width: 96.0, height: 36.0) + let directionsSpacing: CGFloat = 8.0 + + var drivingButtonTitle = "" + var drivingButtonHasIcon = true + var walkingButtonTitle = "" + var drivingButtonVisible = false + var walkingButtonVisible = false + if case let .ready(drivingTime) = item.drivingTime { - strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) + drivingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) ?? "" + drivingButtonVisible = true if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime { - strongSelf.drivingButtonNode?.alpha = 1.0 - strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.drivingButton.view?.alpha = 1.0 + strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - } - - if case let .ready(transitTime) = item.transitTime { - strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 }) - - if let previousTransitTime = currentItem?.transitTime, case .calculating = previousTransitTime { - strongSelf.transitButtonNode?.alpha = 1.0 - strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } else { + drivingButtonVisible = true + if case .unknown = item.walkingTime { + drivingButtonHasIcon = false + drivingButtonTitle = item.presentationData.strings.Map_GetDirections + directionsSize.width = contentSize.width - leftInset * 2.0 } } if case let .ready(walkingTime) = item.walkingTime { - strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) + walkingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) ?? "" + walkingButtonVisible = true if let previousWalkingTime = currentItem?.walkingTime, case .calculating = previousWalkingTime { - strongSelf.walkingButtonNode?.alpha = 1.0 - strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + strongSelf.walkingButton.view?.alpha = 1.0 + strongSelf.walkingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - let directionsWidth: CGFloat = 93.0 - let directionsSpacing: CGFloat = 8.0 - let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 - let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0 + var drivingButtonContent: [AnyComponentWithIdentity] = [] + if drivingButtonHasIcon { + drivingButtonContent.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsDriving", tintColor: foregroundColor)))) + } + drivingButtonContent.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(14.0), color: foregroundColor)))) + + let drivingButtonSize = strongSelf.drivingButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("driving-\(drivingButtonTitle)"), + component: AnyComponent( + HStack(drivingButtonContent, spacing: 2.0) + ) + ), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + if let item = self?.item { + item.drivingAction() + } + } + )), + environment: {}, + containerSize: directionsSize + ) + + let walkingButtonSize = strongSelf.walkingButton.update( + transition: .immediate, + component: AnyComponent(ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("walking-\(walkingButtonTitle)"), + component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsWalking", tintColor: foregroundColor))), + AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: walkingButtonTitle, font: Font.semibold(14.0), color: foregroundColor))) + ], spacing: 0.0) + ) + ), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + if let item = self?.item { + item.walkingAction() + } + } + )), + environment: {}, + containerSize: directionsSize + ) var buttonOrigin = leftInset - strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight)) + let drivingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: drivingButtonSize) + if let drivingButtonView = strongSelf.drivingButton.view { + if drivingButtonView.superview == nil { + strongSelf.view.addSubview(drivingButtonView) + } + drivingButtonView.frame = drivingButtonFrame + if drivingButtonView.layer.animation(forKey: "opacity") == nil { + drivingButtonView.alpha = drivingButtonVisible ? 1.0 : 0.0 + } + } if case .ready = item.drivingTime { - buttonOrigin += directionsWidth + directionsSpacing + buttonOrigin += directionsSize.width + directionsSpacing } - strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight)) - - if case .ready = item.transitTime { - buttonOrigin += directionsWidth + directionsSpacing + let walkingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: walkingButtonSize) + if let walkingButtonView = strongSelf.walkingButton.view { + if walkingButtonView.superview == nil { + strongSelf.view.addSubview(walkingButtonView) + } + walkingButtonView.frame = walkingButtonFrame + if walkingButtonView.layer.animation(forKey: "opacity") == nil { + walkingButtonView.alpha = walkingButtonVisible ? 1.0 : 0.0 + } } - - strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight)) } }) }) diff --git a/submodules/LocationUI/Sources/LocationLiveLocationContextMenu.swift b/submodules/LocationUI/Sources/LocationLiveLocationContextMenu.swift new file mode 100644 index 0000000000..d79e5bb5e1 --- /dev/null +++ b/submodules/LocationUI/Sources/LocationLiveLocationContextMenu.swift @@ -0,0 +1,68 @@ +import UIKit +import Display +import ContextUI +import TelegramPresentationData +import TelegramCore + +private final class LocationLiveLocationReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} + +func makeLiveLocationDurationContextController( + presentationData: PresentationData, + sourceView: UIView, + title: String, + selectPeriod: @escaping (Int32) -> Void +) -> ViewController { + let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem( + text: title, + textLayout: .multiline, + textFont: .small, + parseMarkdown: true, + icon: { _ in + return nil + }, + action: noAction + ))) + + items.append(.separator) + + let periodItems: [(String, Int32)] = [ + (presentationData.strings.Map_LiveLocationForMinutes(15), 15 * 60), + (presentationData.strings.Map_LiveLocationForHours(1), 60 * 60 - 1), + (presentationData.strings.Map_LiveLocationForHours(8), 8 * 60 * 60), + (presentationData.strings.Map_LiveLocationIndefinite, liveLocationIndefinitePeriod) + ] + + for (text, period) in periodItems { + items.append(.action(ContextMenuActionItem( + text: text, + icon: { _ in + return nil + }, + action: { _, f in + f(.default) + selectPeriod(period) + } + ))) + } + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(LocationLiveLocationReferenceContentSource(sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + return contextController +} diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift index c3ef2e37d1..7645090d71 100644 --- a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -2,6 +2,9 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import AccountContext +import TelegramCore +import SwiftSignalKit import TelegramPresentationData import AppBundle import CoreLocation @@ -10,6 +13,8 @@ import GlassBackgroundComponent import PlainButtonComponent import BundleIconComponent import MultilineTextComponent +import LottieComponent +import LottieComponentResourceContent private let panelInset: CGFloat = 4.0 private let panelButtonSize = CGSize(width: 46.0, height: 46.0) @@ -48,6 +53,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { private let goToUserLocation: () -> Void private let showPlacesInThisArea: () -> Void private let setupProximityNotification: (Bool) -> Void + private let weatherPressed: () -> Void private var displayingPlacesButton = false private var proximityNotification: Bool? @@ -67,10 +73,20 @@ public final class LocationMapHeaderNode: ASDisplayNode { private let placesBackgroundView: GlassBackgroundView? private let placesBackgroundNode: ASImageNode private let placesButtonNode: HighlightTrackingButtonNode - private let shadowNode: ASImageNode + private let weatherBackgroundView: GlassBackgroundView? + private let weatherIcon = ComponentView() + private let weatherEmojiLabel = ComponentView() + private let weatherTemperatureLabel = ComponentView() + private let weatherButton: HighlightTrackingButton + private var weatherEmoji: String? + private var weatherTemperature: String? + private var weatherEmojiFile: TelegramMediaFile? + private weak var weatherContext: AccountContext? + private let weatherEmojiLoadDisposable = MetaDisposable() + private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGSize)? - + public init( presentationData: PresentationData, glass: Bool, @@ -79,7 +95,8 @@ public final class LocationMapHeaderNode: ASDisplayNode { updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, setupProximityNotification: @escaping (Bool) -> Void = { _ in }, - showPlacesInThisArea: @escaping () -> Void = {} + showPlacesInThisArea: @escaping () -> Void = {}, + weatherPressed: @escaping () -> Void = {} ) { self.presentationData = presentationData self.glass = glass @@ -88,6 +105,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.goToUserLocation = goToUserLocation self.setupProximityNotification = setupProximityNotification self.showPlacesInThisArea = showPlacesInThisArea + self.weatherPressed = weatherPressed self.mapNode = LocationMapNode() @@ -131,29 +149,35 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.placesBackgroundNode.displayWithoutProcessing = true self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) self.placesBackgroundNode.isUserInteractionEnabled = true - + self.placesButtonNode = HighlightTrackingButtonNode() self.placesButtonNode.setTitle(presentationData.strings.Map_PlacesInThisArea, with: Font.medium(17.0), with: self.glass ? presentationData.theme.rootController.navigationBar.primaryTextColor : buttonColor, for: .normal) - - self.shadowNode = ASImageNode() - self.shadowNode.contentMode = .scaleToFill - self.shadowNode.displaysAsynchronously = false - self.shadowNode.displayWithoutProcessing = true - self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false) - + + self.weatherButton = HighlightTrackingButton() + if glass { self.optionsBackgroundView = GlassContextExtractableContainer() self.optionsBackgroundNode.image = nil self.placesBackgroundView = GlassBackgroundView() self.placesBackgroundNode.image = nil + + self.weatherBackgroundView = GlassBackgroundView() } else { self.optionsBackgroundView = nil self.placesBackgroundView = nil + self.weatherBackgroundView = nil } - + super.init() - + + self.mapNode.visibleRegionDidChange = { [weak self] in + guard let self, self.glass, self.mapNode.mapMode == .satellite else { + return + } + self.requestLayout(transition: .immediate) + } + self.clipsToBounds = true self.addSubnode(self.mapNode) @@ -176,15 +200,28 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.optionsBackgroundView?.contentView.addSubview(self.notificationButtonNode.view) } } - + self.addSubnode(self.placesBackgroundNode) self.placesBackgroundView?.contentView.addSubview(self.placesButtonNode.view) - //self.addSubnode(self.shadowNode) - + if let weatherBackgroundView = self.weatherBackgroundView { + weatherBackgroundView.isHidden = true + self.view.addSubview(weatherBackgroundView) + } + self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) self.locationButtonNode.addTarget(self, action: #selector(self.locationPressed), forControlEvents: .touchUpInside) self.notificationButtonNode.addTarget(self, action: #selector(self.notificationPressed), forControlEvents: .touchUpInside) self.placesButtonNode.addTarget(self, action: #selector(self.placesPressed), forControlEvents: .touchUpInside) + + self.weatherButton.addTarget(self, action: #selector(self.weatherButtonPressed), for: .touchUpInside) + } + + deinit { + self.weatherEmojiLoadDisposable.dispose() + } + + @objc private func weatherButtonPressed() { + self.weatherPressed() } public func updateState(mapMode: LocationMapMode, trackingMode: LocationTrackingMode, displayingMapModeOptions: Bool, displayingPlacesButton: Bool, proximityNotification: Bool?, animated: Bool) { @@ -214,6 +251,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { let buttonColor = self.glass ? presentationData.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.62) : presentationData.theme.rootController.navigationBar.buttonColor + self.mapNode.isDark = presentationData.theme.overallDarkAppearance self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) self.optionsSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor self.optionsSecondSeparatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor @@ -227,9 +265,45 @@ public final class LocationMapHeaderNode: ASDisplayNode { if !self.glass { self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) } - self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false) } + func updateWeatherData(context: AccountContext, emoji: String, temperature: String, animated: Bool) { + let emojiFile = context.animatedEmojiStickersValue[emoji]?.first?.file._parse() + if self.weatherEmoji == emoji && self.weatherTemperature == temperature && self.weatherEmojiFile?.fileId == emojiFile?.fileId { + return + } + + if self.weatherEmojiFile?.fileId != emojiFile?.fileId { + if let emojiFile { + self.weatherEmojiLoadDisposable.set(context.engine.resources.fetch(reference: .standalone(resource: emojiFile.resource), userLocation: .other, userContentType: .sticker).start()) + } else { + self.weatherEmojiLoadDisposable.set(nil) + } + } + + self.weatherContext = context + self.weatherEmoji = emoji + self.weatherTemperature = temperature + self.weatherEmojiFile = emojiFile + self.requestLayout(transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) + } + + func clearWeatherData(animated: Bool) { + if self.weatherEmoji == nil && self.weatherTemperature == nil { + return + } + self.weatherEmoji = nil + self.weatherTemperature = nil + self.weatherEmojiFile = nil + self.weatherContext = nil + self.weatherEmojiLoadDisposable.set(nil) + if let weatherIconView = self.weatherIcon.view as? LottieComponent.View { + weatherIconView.externalShouldPlay = false + } + self.weatherIcon.view?.removeFromSuperview() + self.requestLayout(transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) + } + private func iconForTracking() -> UIImage? { switch self.trackingMode { case .none: @@ -241,7 +315,23 @@ public final class LocationMapHeaderNode: ASDisplayNode { } } - public func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, topPadding: CGFloat, controlsTopPadding: CGFloat, controlsBottomPadding: CGFloat, offset: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) { + func requestLayout(transition: ContainedViewLayoutTransition) { + guard let (layout, navigationBarHeight, topPadding, controlsTopPadding, controlsBottomPadding, offset, size) = self.validLayout else { + return + } + self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, controlsTopPadding: controlsTopPadding, controlsBottomPadding: controlsBottomPadding, offset: offset, size: size, transition: transition) + } + + public func updateLayout( + layout: ContainerViewLayout, + navigationBarHeight: CGFloat, + topPadding: CGFloat, + controlsTopPadding: CGFloat, + controlsBottomPadding: CGFloat, + offset: CGFloat, + size: CGSize, + transition: ContainedViewLayoutTransition + ) { self.validLayout = (layout, navigationBarHeight, topPadding, controlsTopPadding, controlsBottomPadding, offset, size) let mapHeight: CGFloat = floor(layout.size.height * 1.3) + layout.intrinsicInsets.top * 2.0 @@ -265,8 +355,126 @@ public final class LocationMapHeaderNode: ASDisplayNode { transition.updateAlpha(node: self.placesBackgroundNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) transition.updateAlpha(node: self.placesButtonNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) - transition.updateFrame(node: self.shadowNode, frame: CGRect(x: 0.0, y: size.height - 14.0, width: size.width, height: 14.0)) - + if let weatherBackgroundView = self.weatherBackgroundView { + let panelHeight: CGFloat = 36.0 + let horizontalInset: CGFloat = 10.0 + let labelSpacing: CGFloat = 5.0 + let iconSize = CGSize(width: floor(panelHeight * 0.71), height: floor(panelHeight * 0.71)) + let componentTransition = ComponentTransition(transition) + let temperatureSize = self.weatherTemperatureLabel.update( + transition: componentTransition, + component: AnyComponent(Text( + text: self.weatherTemperature ?? "", + font: Font.semibold(15.0), + color: self.presentationData.theme.rootController.navigationBar.primaryTextColor + )), + environment: {}, + containerSize: CGSize(width: 72.0, height: panelHeight) + ) + let emojiSize = self.weatherEmojiLabel.update( + transition: componentTransition, + component: AnyComponent(Text( + text: self.weatherEmoji ?? "", + font: Font.regular(18.0), + color: self.presentationData.theme.rootController.navigationBar.primaryTextColor + )), + environment: {}, + containerSize: iconSize + ) + let panelWidth = max(62.0, ceil(horizontalInset * 2.0 + iconSize.width + labelSpacing + temperatureSize.width)) + let panelFrame = CGRect( + x: layout.safeInsets.left + 16.0, + y: navigationBarHeight + topPadding + 14.0, + width: panelWidth, + height: panelHeight + ) + transition.updateFrame(view: weatherBackgroundView, frame: panelFrame) + + let weatherAlpha: CGFloat = self.weatherEmoji != nil && self.weatherTemperature != nil && size.height > 160.0 + navigationBarHeight && !self.forceIsHidden ? 1.0 : 0.0 + if weatherBackgroundView.isHidden && weatherAlpha > 0.0 { + weatherBackgroundView.isHidden = false + } + weatherBackgroundView.update(size: panelFrame.size, cornerRadius: panelFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, isVisible: weatherAlpha > 0.0, transition: ComponentTransition(transition)) + + let iconFrame = CGRect( + x: horizontalInset, + y: floorToScreenPixels((panelHeight - iconSize.height) / 2.0), + width: iconSize.width, + height: iconSize.height + ) + let temperatureFrame = CGRect( + x: iconFrame.maxX + labelSpacing, + y: floorToScreenPixels((panelHeight - temperatureSize.height) / 2.0), + width: temperatureSize.width, + height: temperatureSize.height + ) + let emojiFrame = CGRect( + x: iconFrame.minX + floorToScreenPixels((iconFrame.width - emojiSize.width) / 2.0), + y: iconFrame.minY + floorToScreenPixels((iconFrame.height - emojiSize.height) / 2.0), + width: emojiSize.width, + height: emojiSize.height + ) + if let weatherEmojiView = self.weatherEmojiLabel.view { + if weatherEmojiView.superview == nil { + weatherBackgroundView.contentView.addSubview(weatherEmojiView) + } + transition.updateFrame(view: weatherEmojiView, frame: emojiFrame) + } + if let weatherTemperatureView = self.weatherTemperatureLabel.view { + if weatherTemperatureView.superview == nil { + weatherBackgroundView.contentView.addSubview(weatherTemperatureView) + } + transition.updateFrame(view: weatherTemperatureView, frame: temperatureFrame) + } + + if let weatherContext = self.weatherContext, let weatherEmojiFile = self.weatherEmojiFile { + var weatherIconTransition = transition + let _ = self.weatherIcon.update( + transition: ComponentTransition(transition), + component: AnyComponent( + LottieComponent( + content: LottieComponent.ResourceContent(context: weatherContext, file: weatherEmojiFile, attemptSynchronously: false, providesPlaceholder: true), + placeholderColor: self.presentationData.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.1), + renderingScale: 2.0, + loop: true + ) + ), + environment: {}, + containerSize: iconSize + ) + if let weatherIconView = self.weatherIcon.view { + if weatherIconView.superview == nil { + weatherIconTransition = .immediate + weatherBackgroundView.contentView.addSubview(weatherIconView) + } + weatherIconTransition.updateFrame(view: weatherIconView, frame: iconFrame) + ComponentTransition(transition).setAlpha(view: weatherIconView, alpha: 1.0) + if let weatherIconView = weatherIconView as? LottieComponent.View { + weatherIconView.externalShouldPlay = weatherAlpha > 0.0 + } + } + if let weatherEmojiView = self.weatherEmojiLabel.view { + componentTransition.setAlpha(view: weatherEmojiView, alpha: 0.0) + } + } else { + if let weatherIconView = self.weatherIcon.view { + componentTransition.setAlpha(view: weatherIconView, alpha: 0.0) + if let weatherIconView = weatherIconView as? LottieComponent.View { + weatherIconView.externalShouldPlay = false + } + } + if let weatherEmojiView = self.weatherEmojiLabel.view { + componentTransition.setAlpha(view: weatherEmojiView, alpha: 1.0) + } + } + componentTransition.setAlpha(view: weatherBackgroundView, alpha: weatherAlpha) + + if self.weatherButton.superview == nil { + weatherBackgroundView.contentView.addSubview(self.weatherButton) + } + self.weatherButton.frame = CGRect(origin: .zero, size: panelFrame.size) + } + if let options = self.options { let optionsSize = options.update( transition: ComponentTransition(transition), @@ -275,24 +483,35 @@ public final class LocationMapHeaderNode: ASDisplayNode { theme: self.presentationData.theme, strings: self.presentationData.strings, mapMode: self.mapNode.mapMode, + currentCoordinate: self.mapNode.mapCenterCoordinate, + trackingMode: self.mapNode.trackingMode, showMapModes: self.infoButtonNode.isSelected, + proximityNotification: self.proximityNotification, updateMapMode: { [weak self] mode in guard let self else { return } self.updateMapMode(mode) + self.requestLayout(transition: .immediate) }, goToUserLocation: { [weak self] in guard let self else { return } self.goToUserLocation() + self.requestLayout(transition: .immediate) }, requestedMapModes: { [weak self] in guard let self else { return } self.toggleMapModeSelection() + }, + setupProximityNotification: { [weak self] reset in + guard let self else { + return + } + self.setupProximityNotification(reset) } ) ), @@ -343,12 +562,17 @@ public final class LocationMapHeaderNode: ASDisplayNode { } } } - - public func updateHighlight(_ highlighted: Bool) { - self.shadowNode.image = generateShadowImage(theme: self.presentationData.theme, highlighted: highlighted) - } - + public func proximityButtonFrame() -> CGRect? { + if let options = self.options { + guard let optionsView = options.view as? LocationOptionsComponent.View else { + return nil + } + return optionsView.proximityButtonFrame().flatMap { frame in + return optionsView.convert(frame, to: self.view) + } + } + if self.notificationButtonNode.alpha > 0.0 { return self.optionsBackgroundNode.view.convert(self.notificationButtonNode.frame, to: self.view) } else { @@ -376,34 +600,46 @@ public final class LocationMapHeaderNode: ASDisplayNode { } -public final class LocationOptionsComponent: Component { - public let theme: PresentationTheme - public let strings: PresentationStrings - public let mapMode: LocationMapMode - public let showMapModes: Bool - public let updateMapMode: (LocationMapMode) -> Void - public let goToUserLocation: () -> Void - public let requestedMapModes: () -> Void +final class LocationOptionsComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let mapMode: LocationMapMode + let currentCoordinate: CLLocationCoordinate2D? + let trackingMode: LocationTrackingMode + let showMapModes: Bool + let proximityNotification: Bool? + let updateMapMode: (LocationMapMode) -> Void + let goToUserLocation: () -> Void + let requestedMapModes: () -> Void + let setupProximityNotification: (Bool) -> Void - public init( + init( theme: PresentationTheme, strings: PresentationStrings, mapMode: LocationMapMode, + currentCoordinate: CLLocationCoordinate2D?, + trackingMode: LocationTrackingMode, showMapModes: Bool, + proximityNotification: Bool?, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, - requestedMapModes: @escaping () -> Void + requestedMapModes: @escaping () -> Void, + setupProximityNotification: @escaping (Bool) -> Void ) { self.theme = theme self.strings = strings self.mapMode = mapMode + self.currentCoordinate = currentCoordinate + self.trackingMode = trackingMode self.showMapModes = showMapModes + self.proximityNotification = proximityNotification self.updateMapMode = updateMapMode self.goToUserLocation = goToUserLocation self.requestedMapModes = requestedMapModes + self.setupProximityNotification = setupProximityNotification } - public static func ==(lhs: LocationOptionsComponent, rhs: LocationOptionsComponent) -> Bool { + static func ==(lhs: LocationOptionsComponent, rhs: LocationOptionsComponent) -> Bool { if lhs.theme !== rhs.theme { return false } @@ -413,13 +649,43 @@ public final class LocationOptionsComponent: Component { if lhs.mapMode != rhs.mapMode { return false } + if lhs.mapMode == .satellite && LocationOptionsComponent.satelliteIconName(coordinate: lhs.currentCoordinate) != LocationOptionsComponent.satelliteIconName(coordinate: rhs.currentCoordinate) { + return false + } + if lhs.trackingMode != rhs.trackingMode { + return false + } if lhs.showMapModes != rhs.showMapModes { return false } + if lhs.proximityNotification != rhs.proximityNotification { + return false + } return true } - public final class View: HighlightTrackingButton { + private static func satelliteIconName(coordinate: CLLocationCoordinate2D?) -> String { + guard let coordinate else { + return "Location/OptionGlobeEurope" + } + + var longitude = coordinate.longitude.truncatingRemainder(dividingBy: 360.0) + if longitude < -180.0 { + longitude += 360.0 + } else if longitude > 180.0 { + longitude -= 360.0 + } + + if longitude >= -170.0 && longitude < -25.0 { + return "Location/OptionGlobeAmerica" + } else if longitude >= -25.0 && longitude < 60.0 { + return "Location/OptionGlobeEurope" + } else { + return "Location/OptionGlobeAsia" + } + } + + final class View: HighlightTrackingButton { private let containerView: GlassBackgroundContainerView private let backgroundView: GlassBackgroundView private let clippingView: UIView @@ -427,6 +693,7 @@ public final class LocationOptionsComponent: Component { private let collapsedContainerView = UIView() private var mapModeButton = ComponentView() private var trackingButton = ComponentView() + private var notificationButton = ComponentView() private let expandedContainerView = UIView() private let checkIcon = UIImageView() @@ -448,7 +715,7 @@ public final class LocationOptionsComponent: Component { self.addSubview(self.containerView) self.containerView.contentView.addSubview(self.backgroundView) - self.addSubview(self.clippingView) + self.backgroundView.contentView.addSubview(self.clippingView) self.clippingView.addSubview(self.collapsedContainerView) self.clippingView.addSubview(self.expandedContainerView) } @@ -566,7 +833,7 @@ public final class LocationOptionsComponent: Component { transition.setFrame(view: hybridButtonView, frame: hybridButtonFrame) } - let normalSize = CGSize(width: 40.0, height: 80.0) + let normalSize = CGSize(width: 40.0, height: component.proximityNotification != nil ? 120.0 : 80.0) let expandedFrame = CGRect(origin: .zero, size: expandedSize) let collapsedFrame = CGRect(origin: CGPoint(x: expandedSize.width - normalSize.width, y: expandedSize.height - normalSize.height), size: normalSize) @@ -575,17 +842,26 @@ public final class LocationOptionsComponent: Component { self.backgroundView.update(size: effectiveBackgroundFrame.size, cornerRadius: cornerRadius, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) transition.setFrame(view: self.backgroundView, frame: effectiveBackgroundFrame) - transition.setFrame(view: self.clippingView, frame: effectiveBackgroundFrame) + transition.setFrame(view: self.clippingView, frame: CGRect(origin: .zero, size: effectiveBackgroundFrame.size)) transition.setFrame(view: self.expandedContainerView, frame: expandedFrame.offsetBy(dx: effectiveBackgroundFrame.width - expandedFrame.width, dy: effectiveBackgroundFrame.height - expandedFrame.height)) transition.setFrame(view: self.collapsedContainerView, frame: collapsedFrame.offsetBy(dx: -effectiveBackgroundFrame.minX, dy: -effectiveBackgroundFrame.minY)) + var mapModeIconName: String + switch component.mapMode { + case .map: + mapModeIconName = "Location/OptionMap" + case .satellite: + mapModeIconName = LocationOptionsComponent.satelliteIconName(coordinate: component.currentCoordinate) + case .hybrid: + mapModeIconName = "Location/OptionHybrid" + } let mapModeButtonSize = self.mapModeButton.update( transition: transition, component: AnyComponent( PlainButtonComponent( content: AnyComponent( - BundleIconComponent(name: "Location/OptionMap", tintColor: component.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.62)) + BundleIconComponent(name: mapModeIconName, tintColor: component.theme.chat.inputPanel.panelControlColor) ), minSize: CGSize(width: 40.0, height: 40.0), action: { [weak self] in @@ -608,13 +884,22 @@ public final class LocationOptionsComponent: Component { } transition.setFrame(view: mapModeButtonView, frame: mapModeButtonFrame) } - + + let trackingModeIconName: String + switch component.trackingMode { + case .none: + trackingModeIconName = "Location/OptionLocate" + case .follow: + trackingModeIconName = "Location/OptionLocating" + case .followWithHeading: + trackingModeIconName = "Location/OptionTracking" + } let trackingButtonSize = self.trackingButton.update( transition: transition, component: AnyComponent( PlainButtonComponent( content: AnyComponent( - BundleIconComponent(name: "Location/OptionLocate", tintColor: component.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.62)) + BundleIconComponent(name: trackingModeIconName, tintColor: component.theme.chat.inputPanel.panelControlColor) ), minSize: CGSize(width: 40.0, height: 40.0), action: { [weak self] in @@ -638,6 +923,41 @@ public final class LocationOptionsComponent: Component { transition.setFrame(view: trackingButtonView, frame: trackingButtonFrame) } + if let proximityNotification = component.proximityNotification { + let notificationIconName = proximityNotification ? "Chat/Title Panels/MuteIcon" : "Location/NotificationIcon" + let notificationButtonSize = self.notificationButton.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + BundleIconComponent(name: notificationIconName, tintColor: component.theme.chat.inputPanel.panelControlColor) + ), + minSize: CGSize(width: 40.0, height: 40.0), + action: { [weak self] in + guard let self, let component = self.component, let proximityNotification = component.proximityNotification else { + return + } + component.setupProximityNotification(proximityNotification) + }, + animateAlpha: true, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let notificationButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 80.0), size: notificationButtonSize) + if let notificationButtonView = self.notificationButton.view { + if notificationButtonView.superview == nil { + self.collapsedContainerView.addSubview(notificationButtonView) + } + transition.setFrame(view: notificationButtonView, frame: notificationButtonFrame) + transition.setAlpha(view: notificationButtonView, alpha: 1.0) + } + } else if let notificationButtonView = self.notificationButton.view { + transition.setAlpha(view: notificationButtonView, alpha: 0.0) + } + transition.setAlpha(view: self.collapsedContainerView, alpha: component.showMapModes ? 0.0 : 1.0) transition.setAlpha(view: self.expandedContainerView, alpha: component.showMapModes ? 1.0 : 0.0) @@ -650,6 +970,16 @@ public final class LocationOptionsComponent: Component { public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return self.backgroundView.frame.contains(point) } + + func proximityButtonFrame() -> CGRect? { + guard let component = self.component, component.proximityNotification != nil, !component.showMapModes else { + return nil + } + guard let notificationButtonView = self.notificationButton.view, notificationButtonView.superview != nil, notificationButtonView.alpha > 0.0 else { + return nil + } + return notificationButtonView.convert(notificationButtonView.bounds, to: self) + } } public func makeView() -> View { diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift index 072865eccd..35caf43aef 100644 --- a/submodules/LocationUI/Sources/LocationMapNode.swift +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -15,9 +15,9 @@ public enum LocationMapMode { var mapType: MKMapType { switch self { case .satellite: - return .satellite + return .satelliteFlyover case .hybrid: - return .hybrid + return .hybridFlyover default: return .standard } @@ -135,6 +135,7 @@ private func generateProximityDim(size: CGSize) -> UIImage { } protocol MKMapViewDelegateTarget: AnyObject { + func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) @@ -155,6 +156,10 @@ private final class MKMapViewDelegateImpl: NSObject, MKMapViewDelegate { super.init() } + func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + self.target?.mapViewDidChangeVisibleRegion(mapView) + } + func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { self.target?.mapView(mapView, regionWillChangeAnimated: animated) } @@ -237,12 +242,41 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { return self.view as? LocationMapView } + private func updateUserHeadingTransform(mapHeading: CGFloat) { + if let heading = self.userHeading { + self.headingArrowView?.isHidden = false + self.headingArrowView?.transform = CGAffineTransform(rotationAngle: (heading - mapHeading) / 180.0 * CGFloat.pi) + } else { + self.headingArrowView?.isHidden = true + self.headingArrowView?.transform = CGAffineTransform.identity + } + } + + private func updateHeadingTransforms(mapHeading: CGFloat? = nil) { + guard let mapView = self.mapView else { + return + } + + let mapHeading = mapHeading ?? CGFloat(mapView.camera.heading) + self.updateUserHeadingTransform(mapHeading: mapHeading) + + for annotation in mapView.annotations { + if let annotationView = mapView.view(for: annotation) as? LocationPinAnnotationView { + annotationView.updateMapHeading(mapHeading) + } + } + + self.pickerAnnotationView?.updateMapHeading(mapHeading) + self.customUserLocationAnnotationView?.updateMapHeading(mapHeading) + } + var returnedToUserLocation = true var ignoreRegionChanges = false var isDragging = false var onTouch: (() -> Void)? var beganInteractiveDragging: (() -> Void)? var endedInteractiveDragging: ((CLLocationCoordinate2D) -> Void)? + var visibleRegionDidChange: (() -> Void)? var disableHorizontalTransitionGesture = false var annotationSelected: ((LocationPinAnnotation?) -> Void)? @@ -369,6 +403,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { self.onTouch?() } self.view.addSubview(self.pickerAnnotationContainerView) + self.updateHeadingTransforms() } var isRotateEnabled: Bool = true { @@ -383,6 +418,12 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } } + var isDark: Bool = false { + didSet { + self.mapView?.overrideUserInterfaceStyle = self.isDark ? .dark : .light + } + } + public var trackingMode: LocationTrackingMode = .none { didSet { self.mapView?.userTrackingMode = self.trackingMode.userTrackingMode @@ -391,6 +432,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } else if self.trackingMode != .followWithHeading && self.headingArrowView?.image == nil { self.headingArrowView?.image = generateHeadingArrowImage() } + self.updateHeadingTransforms() } } @@ -460,6 +502,10 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } } + public func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + self.updateHeadingTransforms(mapHeading: CGFloat(mapView.camera.heading)) + } + public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { guard !self.ignoreRegionChanges, let scrollView = mapView.subviews.first, let gestureRecognizers = scrollView.gestureRecognizers else { return @@ -478,6 +524,9 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + self.updateHeadingTransforms(mapHeading: CGFloat(mapView.camera.heading)) + self.visibleRegionDidChange?() + let wasDragging = self.isDragging if self.isDragging { self.isDragging = false @@ -543,6 +592,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { container.insertSubview(self.proximityDimView, at: 0) } } + self.updateHeadingTransforms(mapHeading: CGFloat(mapView.camera.heading)) } public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { @@ -658,6 +708,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0 + 16.0) self.pickerAnnotationContainerView.addSubview(pickerAnnotationView) self.pickerAnnotationView = pickerAnnotationView + self.updateHeadingTransforms() } else { self.pickerAnnotationView?.removeFromSuperview() self.pickerAnnotationView = nil @@ -696,6 +747,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { self.customUserLocationAnnotationView = annotationView self.pickerAnnotationView?.annotation = annotation + self.updateHeadingTransforms() } else { self.customUserLocationAnnotationView?.removeFromSuperview() self.customUserLocationAnnotationView = nil @@ -705,13 +757,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { public var userHeading: CGFloat? = nil { didSet { - if let heading = self.userHeading { - self.headingArrowView?.isHidden = false - self.headingArrowView?.transform = CGAffineTransform(rotationAngle: CGFloat(heading / 180.0 * CGFloat.pi)) - } else { - self.headingArrowView?.isHidden = true - self.headingArrowView?.transform = CGAffineTransform.identity - } + self.updateHeadingTransforms() } } @@ -869,7 +915,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { } if let compassView = self.compassView { - transition.updateFrame(view: compassView, frame: CGRect(origin: CGPoint(x: size.width - compassView.frame.width - 11.0, y: inset + 110.0 + topPadding), size: compassView.frame.size)) + transition.updateFrame(view: compassView, frame: CGRect(origin: CGPoint(x: size.width - compassView.frame.width - 16.0, y: inset + 14.0 + topPadding), size: compassView.frame.size)) } self.applyPendingSetMapCenter() diff --git a/submodules/LocationUI/Sources/LocationOptionsNode.swift b/submodules/LocationUI/Sources/LocationOptionsNode.swift deleted file mode 100644 index 1036a19a41..0000000000 --- a/submodules/LocationUI/Sources/LocationOptionsNode.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import TelegramPresentationData -import SegmentedControlNode - -public final class LocationOptionsNode: ASDisplayNode { - private var presentationData: PresentationData - - private let backgroundNode: NavigationBackgroundNode - private let separatorNode: ASDisplayNode - private let segmentedControlNode: SegmentedControlNode - - public init(presentationData: PresentationData, hasBackground: Bool = true, updateMapMode: @escaping (LocationMapMode) -> Void) { - self.presentationData = presentationData - - self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) - self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor - - self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: self.presentationData.theme), items: [SegmentedControlItem(title: self.presentationData.strings.Map_Map), SegmentedControlItem(title: self.presentationData.strings.Map_Satellite), SegmentedControlItem(title: self.presentationData.strings.Map_Hybrid)], selectedIndex: 0) - - super.init() - - if hasBackground { - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - } - - self.addSubnode(self.segmentedControlNode) - - self.segmentedControlNode.selectedIndexChanged = { index in - switch index { - case 0: - updateMapMode(.map) - case 1: - updateMapMode(.satellite) - case 2: - updateMapMode(.hybrid) - default: - break - } - } - } - - public func updatePresentationData(_ presentationData: PresentationData) { - self.presentationData = presentationData - self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor - self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme)) - } - - public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundNode.update(size: size, transition: transition) - - transition.updateFrame(node: self.separatorNode, frame: CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel)) - - let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: size.width - 16.0 - leftInset - rightInset), transition: .immediate) - self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floor((size.width - controlSize.width) / 2.0), y: 0.0), size: controlSize) - } -} diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 634e4c5d3f..09634f18a5 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -30,11 +30,10 @@ class LocationPickerInteraction { let updateSearchQuery: (String) -> Void let dismissSearch: () -> Void let dismissInput: () -> Void - let updateSendActionHighlight: (Bool) -> Void let openHomeWorkInfo: () -> Void let showPlacesInThisArea: () -> Void - init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D, Bool) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { + init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D, Bool) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { self.sendLocation = sendLocation self.sendLiveLocation = sendLiveLocation self.sendVenue = sendVenue @@ -46,7 +45,6 @@ class LocationPickerInteraction { self.updateSearchQuery = updateSearchQuery self.dismissSearch = dismissSearch self.dismissInput = dismissInput - self.updateSendActionHighlight = updateSendActionHighlight self.openHomeWorkInfo = openHomeWorkInfo self.showPlacesInThisArea = showPlacesInThisArea } @@ -104,7 +102,15 @@ public final class LocationPickerController: ViewController, AttachmentContainab case legacy } - public init(context: AccountContext, style: Style = .legacy, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: LocationPickerMode, source: Source = .generic, initialLocation: CLLocationCoordinate2D? = nil, completion: @escaping (TelegramMediaMap, Int64?, String?, String?, String?) -> Void) { + public init( + context: AccountContext, + style: Style = .legacy, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + mode: LocationPickerMode, + source: Source = .generic, + initialLocation: CLLocationCoordinate2D? = nil, + completion: @escaping (TelegramMediaMap, Int64?, String?, String?, String?) -> Void + ) { self.context = context self.style = style self.mode = mode @@ -119,11 +125,12 @@ public final class LocationPickerController: ViewController, AttachmentContainab super.init(navigationBarPresentationData: navigationBarPresentationData) self.navigationPresentation = .modal + self._hasGlassStyle = true - if case .glass = style { + if case .share = mode { self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) } else { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) } self.updateBarButtons() @@ -179,44 +186,25 @@ public final class LocationPickerController: ViewController, AttachmentContainab guard let strongSelf = self, authorized else { return } - let controller = ActionSheetController(presentationData: strongSelf.presentationData) var title = strongSelf.presentationData.strings.Map_LiveLocationGroupNewDescription if case let .share(peer, _, _) = strongSelf.mode, let peer = peer, case .user = peer { title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(peer.compactDisplayTitle).string } - let sendLiveLocationImpl: (Int32) -> Void = { [weak self, weak controller] period in - controller?.dismissAnimated() - guard let self, let controller else { - return + let sourceView = strongSelf.controllerNode.liveLocationActionSourceView(extend: false) ?? strongSelf.view + let controller = makeLiveLocationDurationContextController( + presentationData: strongSelf.presentationData, + sourceView: sourceView!, + title: title, + selectPeriod: { [weak self] period in + guard let self else { + return + } + self.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period), nil, nil, nil, nil) + self.dismiss() } - controller.dismissAnimated() - self.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period), nil, nil, nil, nil) - self.dismiss() - } - - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: title, font: .large, parseMarkdown: true), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForMinutes(15), color: .accent, action: { sendLiveLocationImpl(15 * 60) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(1), color: .accent, action: { - sendLiveLocationImpl(60 * 60 - 1) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(8), color: .accent, action: { - sendLiveLocationImpl(8 * 60 * 60) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationIndefinite, color: .accent, action: { - sendLiveLocationImpl(liveLocationIndefinitePeriod) - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in - controller?.dismissAnimated() - }) - ]) - ]) - strongSelf.present(controller, in: .window(.root)) + ) + strongSelf.presentInGlobalOverlay(controller) }) }, sendVenue: { [weak self] venue, queryId, resultId in guard let strongSelf = self else { @@ -316,11 +304,6 @@ public final class LocationPickerController: ViewController, AttachmentContainab } self.searchNavigationContentNode?.deactivate() self.controllerNode.deactivateInput() - }, updateSendActionHighlight: { [weak self] highlighted in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.updateSendActionHighlight(highlighted) }, openHomeWorkInfo: { [weak self] in guard let strongSelf = self else { return diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index b9a825cc98..5123a26635 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -171,10 +171,9 @@ private enum LocationPickerEntry: Comparable, Identifiable { if let coordinate = coordinate { interaction?.sendLocation(coordinate, name, address?.withUpdated(street: nil)) } - }, highlighted: { highlighted in - interaction?.updateSendActionHighlight(highlighted) + }, highlighted: { _ in }) - case let .location(_, title, subtitle, venue, queryId, resultId, coordinate, name, address, isTop): + case let .location(_, title, subtitle, venue, queryId, resultId, coordinate, name, address, _): let icon: LocationActionListItemIcon if let venue = venue { icon = .venue(venue) @@ -187,10 +186,7 @@ private enum LocationPickerEntry: Comparable, Identifiable { } else if let coordinate { interaction?.sendLocation(coordinate, name, address) } - }, highlighted: { highlighted in - if isTop { - interaction?.updateSendActionHighlight(highlighted) - } + }, highlighted: { _ in }) case let .liveLocation(_, title, subtitle, coordinate): return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: engine, title: title, subtitle: subtitle, icon: .liveLocation, beginTimeAndTimeout: nil, action: { @@ -361,7 +357,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM private var sendButton: ComponentView? - private let optionsNode: LocationOptionsNode private(set) var searchContainerNode: LocationSearchContainerNode? private var placeholderBackgroundNode: NavigationBackgroundNode? @@ -422,9 +417,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM showPlacesInThisArea: interaction.showPlacesInThisArea ) self.headerNode.mapNode.isRotateEnabled = false - - self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode) - + self.shadeNode = ASDisplayNode() self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.shadeNode.alpha = 0.0 @@ -443,7 +436,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM self.addSubnode(self.listNode) self.addSubnode(self.headerNode) - //self.addSubnode(self.optionsNode) + self.listNode.addSubnode(self.emptyResultsTextNode) self.shadeNode.addSubnode(self.innerShadeNode) self.addSubnode(self.shadeNode) @@ -1099,7 +1092,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.headerNode.updatePresentationData(self.presentationData) - self.optionsNode.updatePresentationData(self.presentationData) self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.innerShadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.searchContainerNode?.updatePresentationData(self.presentationData) @@ -1214,6 +1206,16 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM return (self.state.selectedLocation.isCustom || self.state.forceSelection) && !self.state.searchingVenuesAround } + func liveLocationActionSourceView(extend: Bool) -> UIView? { + var result: UIView? + self.listNode.forEachItemNode { itemNode in + if result == nil, let itemNode = itemNode as? LocationActionListItemNode { + result = itemNode.liveLocationContextSourceView(extend: extend) + } + } + return result + } + func requestLayout(transition: ContainedViewLayoutTransition) { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) @@ -1291,12 +1293,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } } - let optionsOffset: CGFloat = self.state.displayingMapModeOptions ? navigationHeight : navigationHeight - optionsHeight - let optionsFrame = CGRect(x: 0.0, y: optionsOffset, width: layout.size.width, height: optionsHeight) - transition.updateFrame(node: self.optionsNode, frame: optionsFrame) - self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: insets.left, rightInset: insets.right, transition: transition) - self.optionsNode.isUserInteractionEnabled = self.state.displayingMapModeOptions - if let searchContainerNode = self.searchContainerNode { searchContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size) searchContainerNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationHeight, transition: transition) @@ -1655,11 +1651,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } } - func updateSendActionHighlight(_ highlighted: Bool) { - self.headerNode.updateHighlight(highlighted) - self.shadeNode.backgroundColor = highlighted ? self.presentationData.theme.list.itemHighlightedBackgroundColor : self.presentationData.theme.list.plainBackgroundColor - } - func goToUserLocation() { guard let controller = self.controller else { return diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 637782af41..6b74ecc142 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -31,7 +31,7 @@ public class LocationViewParams { } } -class LocationViewInteraction { +final class LocationViewInteraction { let toggleMapModeSelection: () -> Void let updateMapMode: (LocationMapMode) -> Void let toggleTrackingMode: () -> Void @@ -39,12 +39,11 @@ class LocationViewInteraction { let requestDirections: (TelegramMediaMap, String?, OpenInLocationDirections) -> Void let share: () -> Void let setupProximityNotification: (Bool, EngineMessage.Id?) -> Void - let updateSendActionHighlight: (Bool) -> Void let sendLiveLocation: (Int32?, Bool, EngineMessage.Id?) -> Void let stopLiveLocation: () -> Void let present: (ViewController) -> Void - init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, present: @escaping (ViewController) -> Void) { + init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, present: @escaping (ViewController) -> Void) { self.toggleMapModeSelection = toggleMapModeSelection self.updateMapMode = updateMapMode self.toggleTrackingMode = toggleTrackingMode @@ -52,7 +51,6 @@ class LocationViewInteraction { self.requestDirections = requestDirections self.share = share self.setupProximityNotification = setupProximityNotification - self.updateSendActionHighlight = updateSendActionHighlight self.sendLiveLocation = sendLiveLocation self.stopLiveLocation = stopLiveLocation self.present = present @@ -65,10 +63,11 @@ public final class LocationViewController: ViewController { } private let context: AccountContext public var subject: EngineMessage + private var basePresentationData: PresentationData private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private var currentMapMode: LocationMapMode = .map private var showAll: Bool - private let isStoryLocation: Bool private let isPreview: Bool private let locationManager = LocationManager() @@ -77,31 +76,36 @@ public final class LocationViewController: ViewController { public var dismissed: () -> Void = {} - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, subject: EngineMessage, isStoryLocation: Bool = false, isPreview: Bool = false, params: LocationViewParams) { + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + subject: EngineMessage, + isPreview: Bool = false, + params: LocationViewParams + ) { self.context = context self.subject = subject self.showAll = params.showAll - self.isStoryLocation = isStoryLocation self.isPreview = isPreview - self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + let initialPresentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + self.basePresentationData = initialPresentationData + self.presentationData = LocationViewController.effectivePresentationData(initialPresentationData, mapMode: .map) super.init(navigationBarPresentationData: nil) self._hasGlassStyle = true + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationPresentation = .modal self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in - guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else { + guard let strongSelf = self, strongSelf.basePresentationData.theme !== presentationData.theme else { return } - strongSelf.presentationData = presentationData - - if strongSelf.isNodeLoaded { - strongSelf.controllerNode.updatePresentationData(presentationData) - } + strongSelf.basePresentationData = presentationData + strongSelf.updateEffectivePresentationData(animated: true) }) self.interaction = LocationViewInteraction(toggleMapModeSelection: { [weak self] in @@ -117,12 +121,14 @@ public final class LocationViewController: ViewController { guard let strongSelf = self else { return } + strongSelf.currentMapMode = mode strongSelf.controllerNode.updateState { state in var state = state state.mapMode = mode state.displayingMapModeOptions = false return state } + strongSelf.updateEffectivePresentationData(animated: true) }, toggleTrackingMode: { [weak self] in guard let strongSelf = self else { return @@ -174,7 +180,7 @@ public final class LocationViewController: ViewController { } } } else { - strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: directions), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil) + strongSelf.push(OpenInOptionsScreen(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: directions), additionalAction: nil, openUrl: params.openUrl)) } }, share: { [weak self] in guard let strongSelf = self else { @@ -184,7 +190,7 @@ public final class LocationViewController: ViewController { let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: { strongSelf.present(context.sharedContext.makeShareController(context: context, params: ShareControllerParams(subject: .mapMedia(location), externalShare: true)), in: .window(.root), with: nil) }) - strongSelf.present(OpenInActionSheetController(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: nil), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil) + strongSelf.push(OpenInOptionsScreen(context: context, updatedPresentationData: updatedPresentationData, item: .location(location: location, directions: nil), additionalAction: shareAction, openUrl: params.openUrl)) } }, setupProximityNotification: { [weak self] reset, messageId in guard let strongSelf = self else { @@ -328,11 +334,6 @@ public final class LocationViewController: ViewController { }) }) } - }, updateSendActionHighlight: { [weak self] highlighted in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.updateSendActionHighlight(highlighted) }, sendLiveLocation: { [weak self] distance, extend, messageId in guard let strongSelf = self else { return @@ -405,7 +406,6 @@ public final class LocationViewController: ViewController { } } |> deliverOnMainQueue).start(next: { peer in - let controller = ActionSheetController(presentationData: strongSelf.presentationData) var title: String if extend { title = strongSelf.presentationData.strings.Map_LiveLocationExtendDescription @@ -415,47 +415,32 @@ public final class LocationViewController: ViewController { title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(peer.compactDisplayTitle).string } } - - let sendLiveLocationImpl: (Int32) -> Void = { [weak controller] period in - controller?.dismissAnimated() - - if extend { - if let messageId { - let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: nil, extendPeriod: period).start() + + let sourceView = strongSelf.controllerNode.liveLocationActionSourceView(extend: extend) ?? strongSelf.view + let controller = makeLiveLocationDurationContextController( + presentationData: strongSelf.presentationData, + sourceView: sourceView!, + title: title, + selectPeriod: { [weak self] period in + guard let strongSelf = self else { + return } - } else { - let _ = (strongSelf.controllerNode.coordinate - |> deliverOnMainQueue).start(next: { coordinate in - params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period)) - }) - strongSelf.controllerNode.showAll() + if extend { + if let messageId { + let _ = context.engine.messages.requestEditLiveLocation(messageId: messageId, stop: false, coordinate: nil, heading: nil, proximityNotificationRadius: nil, extendPeriod: period).start() + } + } else { + let _ = (strongSelf.controllerNode.coordinate + |> deliverOnMainQueue).start(next: { coordinate in + params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period)) + }) + + strongSelf.controllerNode.showAll() + } } - } - - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: title, font: .large, parseMarkdown: true), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForMinutes(15), color: .accent, action: { - sendLiveLocationImpl(15 * 60) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(1), color: .accent, action: { - sendLiveLocationImpl(60 * 60 - 1) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(8), color: .accent, action: { - sendLiveLocationImpl(8 * 60 * 60) - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationIndefinite, color: .accent, action: { - sendLiveLocationImpl(liveLocationIndefinitePeriod) - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in - controller?.dismissAnimated() - }) - ]) - ]) - strongSelf.present(controller, in: .window(.root)) + ) + strongSelf.presentInGlobalOverlay(controller) }) } }) @@ -482,7 +467,40 @@ public final class LocationViewController: ViewController { deinit { self.presentationDataDisposable?.dispose() } - + + private static func effectivePresentationData(_ presentationData: PresentationData, mapMode: LocationMapMode) -> PresentationData { + switch mapMode { + case .satellite, .hybrid: + if presentationData.theme.overallDarkAppearance { + return presentationData + } + let darkTheme = customizeDefaultDarkPresentationTheme( + theme: defaultDarkPresentationTheme, + editing: false, + title: nil, + accentColor: presentationData.theme.list.itemAccentColor, + backgroundColors: [], + bubbleColors: [], + animateBubbleColors: false, + wallpaper: nil, + baseColor: nil + ) + return presentationData.withUpdated(theme: darkTheme) + case .map: + return presentationData + } + } + + private func updateEffectivePresentationData(animated: Bool) { + let presentationData = LocationViewController.effectivePresentationData(self.basePresentationData, mapMode: self.currentMapMode) + self.presentationData = presentationData + self.statusBar.updateStatusBarStyle(presentationData.theme.rootController.statusBarStyle.style, animated: animated) + + if self.isNodeLoaded { + self.controllerNode.updatePresentationData(presentationData) + } + } + public func goToUserLocation(visibleRadius: Double? = nil) { } @@ -502,7 +520,7 @@ public final class LocationViewController: ViewController { return } - self.displayNode = LocationViewControllerNode(context: self.context, controller: self, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation, isPreview: self.isPreview) + self.displayNode = LocationViewControllerNode(context: self.context, controller: self, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isPreview: self.isPreview) self.displayNodeDidLoad() self.controllerNode.onAnnotationsReady = { [weak self] in @@ -511,8 +529,6 @@ public final class LocationViewController: ViewController { } strongSelf.controllerNode.showAll() } - - self.controllerNode.headerNode.mapNode.disableHorizontalTransitionGesture = self.isStoryLocation } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -534,4 +550,3 @@ public final class LocationViewController: ViewController { super.dismiss(completion: completion) } } - diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 5f8ec123c2..bb3bcafb7d 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -22,6 +22,8 @@ import GlassControls import BundleIconComponent import EdgeEffect import MultilineTextComponent +import GlassBackgroundComponent +import Weather func getLocation(from message: EngineMessage) -> TelegramMediaMap? { if let poll = message.media.first(where: { $0 is TelegramMediaPoll } ) as? TelegramMediaPoll, let map = poll.attachedMedia as? TelegramMediaMap { @@ -57,9 +59,9 @@ public enum LocationViewEntryId: Hashable { } public enum LocationViewEntry: Comparable, Identifiable { - case info(PresentationTheme, TelegramMediaMap, String?, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Bool) + case info(PresentationTheme, TelegramMediaMap, String?, Double?, ExpectedTravelTime, ExpectedTravelTime, Bool) case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?, Bool, EngineMessage.Id?) - case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessage, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Int) + case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessage, Double?, ExpectedTravelTime, ExpectedTravelTime, Int) public var stableId: LocationViewEntryId { switch self { @@ -67,15 +69,15 @@ public enum LocationViewEntry: Comparable, Identifiable { return .info case let .toggleLiveLocation(_, _, _, _, _, additional, _): return .toggleLiveLocation(additional) - case let .liveLocation(_, _, _, message, _, _, _, _, _): + case let .liveLocation(_, _, _, message, _, _, _, _): return .liveLocation(message.stableId) } } public static func ==(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool { switch lhs { - case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsHasEta): - if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsHasEta) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsHasEta == rhsHasEta { + case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsDrivingTime, lhsWalkingTime, lhsHasEta): + if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsDrivingTime, rhsWalkingTime, rhsHasEta) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsWalkingTime == rhsWalkingTime, lhsHasEta == rhsHasEta { return true } else { return false @@ -86,8 +88,8 @@ public enum LocationViewEntry: Comparable, Identifiable { } else { return false } - case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsTransitTime, lhsWalkingTime, lhsIndex): - if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsTransitTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsTransitTime == rhsTransitTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex { + case let .liveLocation(lhsTheme, lhsDateTimeFormat, lhsNameDisplayOrder, lhsMessage, lhsDistance, lhsDrivingTime, lhsWalkingTime, lhsIndex): + if case let .liveLocation(rhsTheme, rhsDateTimeFormat, rhsNameDisplayOrder, rhsMessage, rhsDistance, rhsDrivingTime, rhsWalkingTime, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsDrivingTime == rhsDrivingTime, lhsWalkingTime == rhsWalkingTime, lhsIndex == rhsIndex { return true } else { return false @@ -113,11 +115,11 @@ public enum LocationViewEntry: Comparable, Identifiable { case .liveLocation: return true } - case let .liveLocation(_, _, _, _, _, _, _, _, lhsIndex): + case let .liveLocation(_, _, _, _, _, _, _, lhsIndex): switch rhs { case .info, .toggleLiveLocation: return false - case let .liveLocation(_, _, _, _, _, _, _, _, rhsIndex): + case let .liveLocation(_, _, _, _, _, _, _, rhsIndex): return lhsIndex < rhsIndex } } @@ -125,7 +127,7 @@ public enum LocationViewEntry: Comparable, Identifiable { func item(context: AccountContext, presentationData: PresentationData, interaction: LocationViewInteraction?) -> ListViewItem { switch self { - case let .info(_, location, address, distance, drivingTime, transitTime, walkingTime, hasEta): + case let .info(_, location, address, distance, drivingTime, walkingTime, hasEta): let addressString: String? if let address = address { addressString = address @@ -138,12 +140,10 @@ public enum LocationViewEntry: Comparable, Identifiable { } else { distanceString = nil } - return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, hasEta: hasEta, action: { + return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, location: location, address: addressString, distance: distanceString, drivingTime: drivingTime, walkingTime: walkingTime, hasEta: hasEta, action: { interaction?.goToCoordinate(location.coordinate) }, drivingAction: { interaction?.requestDirections(location, nil, .driving) - }, transitAction: { - interaction?.requestDirections(location, nil, .transit) }, walkingAction: { interaction?.requestDirections(location, nil, .walking) }) @@ -164,7 +164,7 @@ public enum LocationViewEntry: Comparable, Identifiable { icon = .liveLocation } - return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { + return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, isOpaque: false, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { if beginTimeAndTimeout != nil { if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { if additional { @@ -178,15 +178,14 @@ public enum LocationViewEntry: Comparable, Identifiable { } else { interaction?.sendLiveLocation(nil, false, nil) } - }, highlighted: { highlight in - interaction?.updateSendActionHighlight(highlight) + }, highlighted: { _ in }) - case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, _): + case let .liveLocation(_, dateTimeFormat, nameDisplayOrder, message, distance, drivingTime, walkingTime, _): var title: String? if let author = message.author { title = author.displayTitle(strings: presentationData.strings, displayOrder: nameDisplayOrder) } - return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, transitTime: transitTime, walkingTime: walkingTime, action: { + return LocationLiveListItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: context, message: message, distance: distance, drivingTime: drivingTime, walkingTime: walkingTime, action: { if let location = getLocation(from: message) { interaction?.goToCoordinate(location.coordinate) } @@ -194,10 +193,6 @@ public enum LocationViewEntry: Comparable, Identifiable { if let location = getLocation(from: message) { interaction?.requestDirections(location, title, .driving) } - }, transitAction: { - if let location = getLocation(from: message) { - interaction?.requestDirections(location, title, .transit) - } }, walkingAction: { if let location = getLocation(from: message) { interaction?.requestDirections(location, title, .walking) @@ -285,7 +280,6 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan private var subject: EngineMessage private let interaction: LocationViewInteraction private let locationManager: LocationManager - private let isStoryLocation: Bool private let isPreview: Bool private var rightBarButtonAction: LocationViewRightBarButton = .none @@ -295,11 +289,13 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan private let title = ComponentView() private let listNode: ListView + let backgroundView = GlassBackgroundView() let headerNode: LocationMapHeaderNode private var enqueuedTransitions: [LocationViewTransaction] = [] private var disposable: Disposable? + private let weatherDisposable = MetaDisposable() private var state: LocationViewState private let statePromise: Promise @@ -312,14 +308,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan var onAnnotationsReady: (() -> Void)? private let travelDisposables = DisposableSet() - private var travelTimes: [EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime)] = [:] { + private var travelTimes: [EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime)] = [:] { didSet { self.travelTimesPromise.set(.single(self.travelTimes)) } } - private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime)]>([:]) + private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime)]>([:]) - init(context: AccountContext, controller: LocationViewController, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool, isPreview: Bool) { + init(context: AccountContext, controller: LocationViewController, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isPreview: Bool) { self.context = context self.controller = controller self.presentationData = presentationData @@ -327,14 +323,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.subject = subject self.interaction = interaction self.locationManager = locationManager - self.isStoryLocation = isStoryLocation self.isPreview = isPreview self.state = LocationViewState() self.statePromise = Promise(self.state) self.listNode = ListViewImpl() - self.listNode.backgroundColor = .clear //self.presentationData.theme.list.plainBackgroundColor + self.listNode.backgroundColor = .clear + self.listNode.limitHitTestToNodes = true self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) self.listNode.verticalScrollIndicatorFollowsOverscroll = true self.listNode.accessibilityPageScrolledString = { row, count in @@ -342,6 +338,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } var setupProximityNotificationImpl: ((Bool) -> Void)? + var weatherPressedImpl: (() -> Void)? self.headerNode = LocationMapHeaderNode( presentationData: presentationData, glass: true, @@ -351,6 +348,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan goToUserLocation: interaction.toggleTrackingMode, setupProximityNotification: { reset in setupProximityNotificationImpl?(reset) + }, + weatherPressed: { + weatherPressedImpl?() } ) @@ -358,35 +358,42 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.addSubnode(self.headerNode) if !self.isPreview { self.addSubnode(self.listNode) } - self.addSubnode(self.headerNode) let userLocation: Signal = .single(nil) |> then( throttledUserLocation(self.headerNode.mapNode.userLocation) ) - var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating)) + var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating)) var address: Signal = .single(nil) + let subjectLocation = getLocation(from: subject) + let isStaticLocationView: Bool + if let subjectLocation { + isStaticLocationView = subjectLocation.liveBroadcastingTimeout == nil + } else { + isStaticLocationView = false + } + let locale = localeWithStrings(presentationData.strings) - if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil { - eta = .single((.calculating, .calculating, .calculating)) - |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)) - |> mapToSignal { drivingTime, transitTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> in + if let location = subjectLocation, isStaticLocationView { + self.headerNode.mapNode.setMapCenter(coordinate: location.coordinate, span: LocationMapNode.viewMapSpan, animated: false) + + eta = .single((.calculating, .calculating)) + |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)) + |> mapToSignal { drivingTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> in if case .calculating = drivingTime { return .complete() } - if case .calculating = transitTime { - return .complete() - } if case .calculating = walkingTime { return .complete() } - return .single((drivingTime, transitTime, walkingTime)) + return .single((drivingTime, walkingTime)) }) if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty { @@ -402,13 +409,21 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } } - let liveLocations = context.engine.messages.topPeerActiveLiveLocationMessages(peerId: subject.id.peerId) + let actualLiveLocations = context.engine.messages.topPeerActiveLiveLocationMessages(peerId: subject.id.peerId) |> map { _, messages -> [EngineMessage] in return messages } + let renderLiveLocations: Signal<[EngineMessage], NoError> + if isStaticLocationView { + renderLiveLocations = .single([]) + |> then(actualLiveLocations) + } else { + renderLiveLocations = actualLiveLocations + } + setupProximityNotificationImpl = { reset in - let _ = (liveLocations + let _ = (actualLiveLocations |> take(1) |> deliverOnMainQueue).start(next: { messages in var ownMessageId: EngineMessage.Id? @@ -424,15 +439,36 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan }) } + weatherPressedImpl = { + if let location = subjectLocation { + context.sharedContext.openExternalUrl( + context: context, + urlContext: .generic, + url: "https://weather.apple.com/?lat=\(location.latitude)&long=\(location.longitude)", + forceExternal: true, + presentationData: presentationData, + navigationController: nil, + dismissInput: {} + ) + } + } + let previousState = Atomic(value: nil) let previousUserAnnotation = Atomic(value: nil) let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) let previousEntries = Atomic<[LocationViewEntry]?>(value: nil) let previousHadTravelTimes = Atomic(value: false) - let selfPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + let actualSelfPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + let renderSelfPeer: Signal + if isStaticLocationView { + renderSelfPeer = .single(nil) + |> then(actualSelfPeer) + } else { + renderSelfPeer = actualSelfPeer + } - self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), selfPeer, liveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta, self.travelTimesPromise.get()) + self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), renderSelfPeer, renderLiveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta, self.travelTimesPromise.get()) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, selfPeer, liveLocations, userLocation, distance, address, eta, travelTimes in if let strongSelf = self, let location = getLocation(from: subject) { var entries: [LocationViewEntry] = [] @@ -453,7 +489,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude) let distance = userLocation.flatMap { subjectLocation.distance(from: $0) } - entries.append(.info(presentationData.theme, location, address, distance, eta.0, eta.1, eta.2, !isStoryLocation)) + entries.append(.info(presentationData.theme, location, address, distance, eta.0, eta.1, true)) annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, location: location, queryId: nil, resultId: nil, forcedSelection: true)) } else { @@ -529,7 +565,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan for message in effectiveLiveLocations { if let location = getLocation(from: message) { - if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, message.threadId != nil { + if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, let threadId = message.threadId, threadId != 1 { continue } @@ -555,60 +591,57 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan userAnnotation = LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: true, heading: location.heading) } else { var drivingTime: ExpectedTravelTime = .unknown - var transitTime: ExpectedTravelTime = .unknown var walkingTime: ExpectedTravelTime = .unknown if !isLocationView && message.author?.id != context.account.peerId { - let signal = combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)) - |> mapToSignal { drivingTime, transitTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> in + let signal = combineLatest( + queue: Queue.mainQueue(), + getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), + getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking) + ) + |> mapToSignal { drivingTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> in if case .calculating = drivingTime { return .complete() } - if case .calculating = transitTime { - return .complete() - } if case .calculating = walkingTime { return .complete() } - - return .single((drivingTime, transitTime, walkingTime)) + return .single((drivingTime, walkingTime)) } - if let (previousTimestamp, maybeDrivingTime, maybeTransitTime, maybeWalkingTime) = travelTimes[message.id] { + if let (previousTimestamp, maybeDrivingTime, maybeWalkingTime) = travelTimes[message.id] { drivingTime = maybeDrivingTime - transitTime = maybeTransitTime walkingTime = maybeWalkingTime if timestamp > previousTimestamp + 60.0 { - strongSelf.travelDisposables.add(signal.start(next: { [weak self] drivingTime, transitTime, walkingTime in + strongSelf.travelDisposables.add(signal.start(next: { [weak self] drivingTime, walkingTime in guard let strongSelf = self else { return } let timestamp = CACurrentMediaTime() var travelTimes = strongSelf.travelTimes - travelTimes[message.id] = (timestamp, drivingTime, transitTime, walkingTime) + travelTimes[message.id] = (timestamp, drivingTime, walkingTime) strongSelf.travelTimes = travelTimes })) } } else { drivingTime = .calculating - transitTime = .calculating walkingTime = .calculating - strongSelf.travelDisposables.add(signal.start(next: { [weak self] drivingTime, transitTime, walkingTime in + strongSelf.travelDisposables.add(signal.start(next: { [weak self] drivingTime, walkingTime in guard let strongSelf = self else { return } let timestamp = CACurrentMediaTime() var travelTimes = strongSelf.travelTimes - travelTimes[message.id] = (timestamp, drivingTime, transitTime, walkingTime) + travelTimes[message.id] = (timestamp, drivingTime, walkingTime) strongSelf.travelTimes = travelTimes })) } } annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, isSelf: message.author?.id == context.account.peerId, heading: location.heading)) - entries.append(.liveLocation(presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, message, distance, drivingTime, transitTime, walkingTime, index)) + entries.append(.liveLocation(presentationData.theme, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, message, distance, drivingTime, walkingTime, index)) } index += 1 } @@ -738,14 +771,11 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan if !isPreview { self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in - guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout, strongSelf.listNode.scrollEnabled else { + guard let self, self.listNode.scrollEnabled else { return } - let overlap: CGFloat = 0.0 - strongSelf.listOffset = max(0.0, offset) - let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap))) - listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame) - strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsTopPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition) + self.listOffset = max(0.0, offset) + self.updateHeader(transition: listTransition) } } @@ -790,14 +820,25 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.locationManager.manager.startUpdatingHeading() self.locationManager.manager.delegate = self + + if !self.isPreview, let location = getLocation(from: subject) { + self.requestWeatherData(coordinate: location.coordinate) + } } deinit { self.disposable?.dispose() + self.weatherDisposable.dispose() self.travelDisposables.dispose() self.locationManager.manager.stopUpdatingHeading() } + override func didLoad() { + super.didLoad() + + self.view.insertSubview(self.backgroundView, aboveSubview: self.headerNode.view) + } + func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { if newHeading.headingAccuracy < 0.0 { self.headerNode.mapNode.userHeading = nil @@ -814,17 +855,70 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.presentationDataPromise.set(.single(presentationData)) self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - self.listNode.backgroundColor = .clear // self.presentationData.theme.list.plainBackgroundColor + self.listNode.backgroundColor = .clear self.headerNode.updatePresentationData(self.presentationData) + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: .immediate) + } } func updateState(_ f: (LocationViewState) -> LocationViewState) { self.state = f(self.state) self.statePromise.set(.single(self.state)) } - - func updateSendActionHighlight(_ highlighted: Bool) { - self.headerNode.updateHighlight(highlighted) + + private func requestWeatherData(coordinate: CLLocationCoordinate2D) { + self.weatherDisposable.set((Weather.requestWeatherData(context: self.context, location: coordinate) + |> deliverOnMainQueue).start(next: { [weak self] weatherData in + guard let self else { + return + } + if let weatherData { + self.headerNode.updateWeatherData(context: self.context, emoji: weatherData.emoji, temperature: stringForTemperature(weatherData.temperature), animated: true) + } else { + self.headerNode.clearWeatherData(animated: true) + } + })) + } + + func updateHeader(transition: ContainedViewLayoutTransition) { + guard let (layout, navigationBarHeight) = self.validLayout else { + return + } + let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) + transition.updateFrame(node: self.headerNode, frame: headerFrame) + + let headerHeight: CGFloat + if self.isPreview { + headerHeight = layout.size.height + } else if let listOffset = self.listOffset { + headerHeight = max(0.0, listOffset) + } else { + headerHeight = headerFrame.height + } + let headerSize = CGSize(width: headerFrame.width, height: headerHeight) + self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: 0.0, controlsTopPadding: 0.0, controlsBottomPadding: 6.0, offset: 0.0, size: headerSize, transition: transition) + + let backgroundHeight = layout.size.height - headerHeight + + let glassInset: CGFloat = 6.0 + let backgroundSize = CGSize(width: layout.size.width - glassInset * 2.0, height: backgroundHeight) + + let bottomCornerRadius = max(24.0, layout.deviceMetrics.screenCornerRadius) - 2.0 + + self.backgroundView.update( + size: backgroundSize, + cornerRadii: .init( + topLeft: 38.0, + topRight: 38.0, + bottomLeft: bottomCornerRadius, + bottomRight: bottomCornerRadius + ), + isDark: self.presentationData.theme.overallDarkAppearance, + tintColor: .init(kind: .panel), + transition: ComponentTransition(transition) + ) + transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: glassInset, y: layout.size.height - backgroundSize.height - glassInset), size: backgroundSize)) } private func enqueueTransition(_ transition: LocationViewTransaction) { @@ -927,6 +1021,16 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.headerNode.mapNode.showAll() } + func liveLocationActionSourceView(extend: Bool) -> UIView? { + var result: UIView? + self.listNode.forEachItemNode { itemNode in + if result == nil, let itemNode = itemNode as? LocationActionListItemNode { + result = itemNode.liveLocationContextSourceView(extend: extend) + } + } + return result + } + private func displayProximityAlertTooltip() { guard let location = self.headerNode.proximityButtonFrame().flatMap({ frame -> CGRect in return self.headerNode.view.convert(frame, to: nil) @@ -962,7 +1066,6 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let isFirstLayout = self.validLayout == nil self.validLayout = (layout, navigationHeight) - let optionsHeight: CGFloat = 38.0 var actionHeight: CGFloat? self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? LocationActionListItemNode { @@ -974,25 +1077,16 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let overlap: CGFloat = 0.0 var topInset: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - overlap - if !self.isStoryLocation { - topInset -= 100.0 - } + topInset -= 100.0 + if let location = getLocation(from: self.subject), location.liveBroadcastingTimeout != nil { topInset += 66.0 } - let headerHeight: CGFloat - if self.isPreview { - headerHeight = layout.size.height - } else if let listOffset = self.listOffset { - headerHeight = max(0.0, listOffset + overlap) - } else { - headerHeight = topInset + overlap + if self.listOffset == nil { + self.listOffset = topInset } - let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) - transition.updateFrame(node: self.headerNode, frame: headerFrame) - - self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: CGSize(width: headerFrame.width, height: headerHeight), transition: transition) + self.updateHeader(transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) @@ -1005,7 +1099,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan if !self.isPreview { let topEdgeEffectFrame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: 80.0)) transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame) - self.topEdgeEffectView.update(content: self.headerNode.mapNode.mapMode == .map ? self.presentationData.theme.list.plainBackgroundColor : .clear, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) + self.topEdgeEffectView.update(content: !self.presentationData.theme.overallDarkAppearance ? self.presentationData.theme.list.plainBackgroundColor : .clear, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) if self.topEdgeEffectView.superview == nil { self.view.addSubview(self.topEdgeEffectView) } diff --git a/submodules/MediaPickerUI/BUILD b/submodules/MediaPickerUI/BUILD index 8088d37de4..eb9d00e7bf 100644 --- a/submodules/MediaPickerUI/BUILD +++ b/submodules/MediaPickerUI/BUILD @@ -36,7 +36,6 @@ swift_library( "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", - "//submodules/WebSearchUI:WebSearchUI", "//submodules/ChatMessageBackground:ChatMessageBackground", "//submodules/SparseItemGrid:SparseItemGrid", "//submodules/UndoUI:UndoUI", @@ -57,6 +56,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/GlassControls", + "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/AlertComponent", "//submodules/Components/BundleIconComponent", diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index 254707296d..7d939ef00c 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -152,6 +152,15 @@ func presentLegacyMediaPickerGallery( legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style let paintStickersContext = LegacyPaintStickersContext(context: context) + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: presentationData, presentInGlobalOverlay: { [weak legacyController] controller in + if let legacyController { + legacyController.presentInGlobalOverlay(controller) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(controller) + } else { + context.sharedContext.presentGlobalController(controller, nil) + } + }) paintStickersContext.captionPanelView = { return getCaptionPanelView() } @@ -165,6 +174,9 @@ func presentLegacyMediaPickerGallery( livePhotoButton.present = present return livePhotoButton } + paintStickersContext.photoToolbarView = { backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) + } paintStickersContext.editCover = { dimensions, completion in editCover(dimensions, completion) } @@ -298,7 +310,7 @@ func presentLegacyMediaPickerGallery( } } if !isScheduledMessages && peer != nil { - model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in + model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model, weak paintStickersContext] item, sourceView in if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext { var effectiveHasSchedule = hasSchedule @@ -344,37 +356,35 @@ func presentLegacyMediaPickerGallery( let _ = (sendWhenOnlineAvailable |> take(1) |> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in - let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) - let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer) let dismissImpl = { [weak model] in model?.dismiss(true, false) dismissAll() } - sheetController.send = { + let send = { completed(item.asset, false, nil, { dismissImpl() }) } - sheetController.sendSilently = { [weak model] in + let sendSilently = { [weak model] in model?.interfaceView.onDismiss() completed(item.asset, true, nil, { dismissImpl() }) } - sheetController.sendWhenOnline = { + let sendWhenOnline = { completed(item.asset, false, scheduleWhenOnlineTimestamp, { dismissImpl() }) } - sheetController.schedule = { + let schedule = { presentSchedulePicker(true, { time, silentPosting in completed(item.asset, silentPosting, time, { dismissImpl() }) }) } - sheetController.sendWithTimer = { + let sendWithTimer = { presentTimerPicker { time in var items = selectionContext.selectedItems() ?? [] items.append(item.asset as Any) @@ -388,6 +398,20 @@ func presentLegacyMediaPickerGallery( }) } } + + if let sourceView, let paintStickersContext, paintStickersContext.presentMediaPickerSendActionMenu?(sourceView, hasSilentPosting, sendWhenOnlineAvailable && effectiveHasSchedule, effectiveHasSchedule, reminder, hasTimer, sendSilently, sendWhenOnline, schedule, sendWithTimer) == true { + let hapticFeedback = HapticFeedback() + hapticFeedback.impact() + return + } + + let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) + let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer) + sheetController.send = send + sheetController.sendSilently = sendSilently + sheetController.sendWhenOnline = sendWhenOnline + sheetController.schedule = schedule + sheetController.sendWithTimer = sendWithTimer sheetController.customDismissBlock = { [weak legacySheetController] in legacySheetController?.dismiss() } diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift b/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift index e2b5f31a29..8f2d4df733 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift @@ -167,6 +167,7 @@ final class MediaGroupsContextMenuContent: ContextControllerItemsContent { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.clipsToBounds = false + self.scrollNode.view.scrollsToTop = false super.init() diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift index 79dbd0c8c9..8171f784fc 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift @@ -470,7 +470,7 @@ public final class MediaGroupsScreen: ViewController, AttachmentContainable { self.updateNavigationStack { current in var mediaPickerContext: AttachmentMediaPickerContext? if let first = current.first as? MediaPickerScreenImpl { - mediaPickerContext = first.webSearchController?.mediaPickerContext ?? first.mediaPickerContext + mediaPickerContext = first.mediaPickerContext } return (current.filter { $0 !== self }, mediaPickerContext) } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift index c48fc25276..a26d7c686a 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerManageNode.swift @@ -15,7 +15,7 @@ final class MediaPickerManageNode: ASDisplayNode { private let measureButtonNode: ImmediateTextNode private let buttonNode: SolidRoundedButtonNode - var pressed: () -> Void = {} + var pressed: (UIView) -> Void = { _ in } override init() { self.textNode = ImmediateTextNode() @@ -33,7 +33,10 @@ final class MediaPickerManageNode: ASDisplayNode { self.addSubnode(self.buttonNode) self.buttonNode.pressed = { [weak self] in - self?.pressed() + guard let self else { + return + } + self.pressed(self.buttonNode.view) } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerPhotoToolbarView.swift b/submodules/MediaPickerUI/Sources/MediaPickerPhotoToolbarView.swift new file mode 100644 index 0000000000..b7ae2d96a4 --- /dev/null +++ b/submodules/MediaPickerUI/Sources/MediaPickerPhotoToolbarView.swift @@ -0,0 +1,1187 @@ +import Foundation +import UIKit +import Display +import LegacyComponents +import AccountContext +import TelegramPresentationData +import ComponentFlow +import GlassBackgroundComponent +import GlassBarButtonComponent +import PlainButtonComponent +import BundleIconComponent + +private let toolbarButtonSide: CGFloat = 44.0 +private let toolbarSideButtonSide: CGFloat = 44.0 +private let centerButtonSpacing: CGFloat = 10.0 + +private let toolbarTabOrder: [TGPhotoEditorTab] = [ + .cropTab, + .stickerTab, + .paintTab, + .eraserTab, + .textTab, + .toolsTab, + .rotateTab, + .qualityTab, + .timerTab, + .mirrorTab, + .aspectRatioTab, + .tintTab, + .blurTab, + .curvesTab +] + +private let dontHighlightOnSelectionTabs = Set([ + TGPhotoEditorTab.rotateTab.rawValue, + TGPhotoEditorTab.stickerTab.rawValue, + TGPhotoEditorTab.textTab.rawValue, + TGPhotoEditorTab.qualityTab.rawValue, + TGPhotoEditorTab.timerTab.rawValue, + TGPhotoEditorTab.mirrorTab.rawValue, + TGPhotoEditorTab.aspectRatioTab.rawValue +]) + + +private final class MediaPickerPhotoToolbarImageCache { + private var images: [String: UIImage] = [:] + + func qualityIcon(isPhoto: Bool, highQuality: Bool, preset: Int, color: UIColor) -> UIImage? { + let key = "quality-\(isPhoto)-\(highQuality)-\(preset)-\(self.colorKey(color))" + if let image = self.images[key] { + return image + } + let image = generateQualityIcon(isPhoto: isPhoto, highQuality: highQuality, preset: preset, color: color) + self.images[key] = image + return image + } + + func timerIcon(value: Int, color: UIColor) -> UIImage? { + let key = "timer-\(value)-\(self.colorKey(color))" + if let image = self.images[key] { + return image + } + let image = generateTimerIcon(value: value, color: color) + self.images[key] = image + return image + } + + private func colorKey(_ color: UIColor) -> String { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { + return "\(Int(red * 255.0))-\(Int(green * 255.0))-\(Int(blue * 255.0))-\(Int(alpha * 255.0))" + } + var white: CGFloat = 0.0 + if color.getWhite(&white, alpha: &alpha) { + return "\(Int(white * 255.0))-\(Int(alpha * 255.0))" + } + return color.description + } +} + +private func generateQualityIcon(isPhoto: Bool, highQuality: Bool, preset: Int, color: UIColor) -> UIImage? { + let label: String + if isPhoto { + label = highQuality ? "HD" : "SD" + } else { + switch preset { + case 1: + label = "240" + case 2: + label = "360" + case 3: + label = "480" + case 4: + label = "720" + case 5: + label = "HD" + default: + label = "480" + } + } + + let size = CGSize(width: isPhoto ? 24.0 : 28.0, height: 24.0) + let lineWidth = 2.0 - UIScreenPixel + let rect = CGRect(origin: .zero, size: size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0) + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + guard let context = UIGraphicsGetCurrentContext() else { + UIGraphicsEndImageContext() + return nil + } + + context.setStrokeColor(color.cgColor) + context.setLineWidth(lineWidth) + context.addPath(UIBezierPath(roundedRect: rect, cornerRadius: 7.0).cgPath) + context.strokePath() + + let font = Font.with(size: 11.0, design: .round, weight: .bold) + let attributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: color, + .kern: -0.7 + ] + let textSize = label.size(withAttributes: attributes) + label.draw( + in: CGRect( + x: floorToScreenPixels((size.width - textSize.width) / 2.0) + UIScreenPixel, + y: 5.0, + width: textSize.width, + height: textSize.height + ), + withAttributes: attributes + ) + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image +} + +private func generateTimerIcon(value: Int, color: UIColor) -> UIImage? { + let size = CGSize(width: 24.0, height: 24.0) + let lineWidth = 2.0 - UIScreenPixel + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + guard let context = UIGraphicsGetCurrentContext() else { + UIGraphicsEndImageContext() + return nil + } + + context.setStrokeColor(color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + + let bodyRect = CGRect(x: 4.0, y: 5.0, width: 16.0, height: 16.0).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0) + context.strokeEllipse(in: bodyRect) + + context.move(to: CGPoint(x: 9.0, y: 2.5)) + context.addLine(to: CGPoint(x: 15.0, y: 2.5)) + context.strokePath() + + context.move(to: CGPoint(x: 12.0, y: 5.0)) + context.addLine(to: CGPoint(x: 12.0, y: 8.0)) + context.strokePath() + + if value == 0 { + context.move(to: CGPoint(x: 12.0, y: 13.0)) + context.addLine(to: CGPoint(x: 12.0, y: 9.0)) + context.move(to: CGPoint(x: 12.0, y: 13.0)) + context.addLine(to: CGPoint(x: 15.0, y: 13.0)) + context.strokePath() + } else { + let label = "\(value)" + let font = Font.with(size: 10.0, design: .round, weight: .semibold) + let attributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: color + ] + let textSize = label.size(withAttributes: attributes) + label.draw( + in: CGRect( + x: floorToScreenPixels((size.width - textSize.width) / 2.0), + y: 9.0, + width: textSize.width, + height: textSize.height + ), + withAttributes: attributes + ) + } + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image +} + +private final class MediaPickerPhotoToolbarComponent: Component { + let context: AccountContext + let solidBackground: Bool + let backButtonType: TGPhotoEditorBackButton + let doneButtonType: TGPhotoEditorDoneButton + let currentTabs: TGPhotoEditorTab + let activeTab: TGPhotoEditorTab + let highlightedTabs: TGPhotoEditorTab + let disabledTabs: TGPhotoEditorTab + let qualityIsPhoto: Bool + let qualityHighQuality: Bool + let qualityPreset: Int + let timerValue: Int + let hasSendStarsButton: Bool + let sendPaidMessageStars: Int64 + let editButtonsHidden: Bool + let editButtonsEnabled: Bool + let centerButtonsHidden: Bool + let allButtonsHidden: Bool + let cancelDoneButtonsHidden: Bool + let doneButtonEnabled: Bool + let interfaceOrientation: UIInterfaceOrientation + let bottomInset: CGFloat + let infoString: String? + let cancelPressed: (() -> Void)? + let donePressed: (() -> Void)? + let doneLongPressed: ((Any?) -> Void)? + let tabPressed: ((TGPhotoEditorTab) -> Void)? + + init( + context: AccountContext, + solidBackground: Bool, + backButtonType: TGPhotoEditorBackButton, + doneButtonType: TGPhotoEditorDoneButton, + currentTabs: TGPhotoEditorTab, + activeTab: TGPhotoEditorTab, + highlightedTabs: TGPhotoEditorTab, + disabledTabs: TGPhotoEditorTab, + qualityIsPhoto: Bool, + qualityHighQuality: Bool, + qualityPreset: Int, + timerValue: Int, + hasSendStarsButton: Bool, + sendPaidMessageStars: Int64, + editButtonsHidden: Bool, + editButtonsEnabled: Bool, + centerButtonsHidden: Bool, + allButtonsHidden: Bool, + cancelDoneButtonsHidden: Bool, + doneButtonEnabled: Bool, + interfaceOrientation: UIInterfaceOrientation, + bottomInset: CGFloat, + infoString: String?, + cancelPressed: (() -> Void)?, + donePressed: (() -> Void)?, + doneLongPressed: ((Any?) -> Void)?, + tabPressed: ((TGPhotoEditorTab) -> Void)? + ) { + self.context = context + self.solidBackground = solidBackground + self.backButtonType = backButtonType + self.doneButtonType = doneButtonType + self.currentTabs = currentTabs + self.activeTab = activeTab + self.highlightedTabs = highlightedTabs + self.disabledTabs = disabledTabs + self.qualityIsPhoto = qualityIsPhoto + self.qualityHighQuality = qualityHighQuality + self.qualityPreset = qualityPreset + self.timerValue = timerValue + self.hasSendStarsButton = hasSendStarsButton + self.sendPaidMessageStars = sendPaidMessageStars + self.editButtonsHidden = editButtonsHidden + self.editButtonsEnabled = editButtonsEnabled + self.centerButtonsHidden = centerButtonsHidden + self.allButtonsHidden = allButtonsHidden + self.cancelDoneButtonsHidden = cancelDoneButtonsHidden + self.doneButtonEnabled = doneButtonEnabled + self.interfaceOrientation = interfaceOrientation + self.bottomInset = bottomInset + self.infoString = infoString + self.cancelPressed = cancelPressed + self.donePressed = donePressed + self.doneLongPressed = doneLongPressed + self.tabPressed = tabPressed + } + + static func ==(lhs: MediaPickerPhotoToolbarComponent, rhs: MediaPickerPhotoToolbarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.solidBackground != rhs.solidBackground { + return false + } + if lhs.backButtonType != rhs.backButtonType { + return false + } + if lhs.doneButtonType != rhs.doneButtonType { + return false + } + if lhs.currentTabs != rhs.currentTabs { + return false + } + if lhs.activeTab != rhs.activeTab { + return false + } + if lhs.highlightedTabs != rhs.highlightedTabs { + return false + } + if lhs.disabledTabs != rhs.disabledTabs { + return false + } + if lhs.qualityIsPhoto != rhs.qualityIsPhoto { + return false + } + if lhs.qualityHighQuality != rhs.qualityHighQuality { + return false + } + if lhs.qualityPreset != rhs.qualityPreset { + return false + } + if lhs.timerValue != rhs.timerValue { + return false + } + if lhs.hasSendStarsButton != rhs.hasSendStarsButton { + return false + } + if lhs.sendPaidMessageStars != rhs.sendPaidMessageStars { + return false + } + if lhs.editButtonsHidden != rhs.editButtonsHidden { + return false + } + if lhs.editButtonsEnabled != rhs.editButtonsEnabled { + return false + } + if lhs.centerButtonsHidden != rhs.centerButtonsHidden { + return false + } + if lhs.allButtonsHidden != rhs.allButtonsHidden { + return false + } + if lhs.cancelDoneButtonsHidden != rhs.cancelDoneButtonsHidden { + return false + } + if lhs.doneButtonEnabled != rhs.doneButtonEnabled { + return false + } + if lhs.interfaceOrientation != rhs.interfaceOrientation { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + if lhs.infoString != rhs.infoString { + return false + } + return true + } + + final class View: UIView { + fileprivate let cancelButton = ComponentView() + fileprivate let doneButton = ComponentView() + private let doneButtonContextView = ContextControllerSourceView() + + private let buttonsBackgroundView = GlassBackgroundView() + private let selectionView = UIView() + private let infoLabel = UILabel() + private let imageCache = MediaPickerPhotoToolbarImageCache() + private var centerButtonViews: [UInt: ComponentView] = [:] + + private var component: MediaPickerPhotoToolbarComponent? + private var cancelPressed: (() -> Void)? + private var donePressed: (() -> Void)? + private var doneLongPressed: ((Any?) -> Void)? + private var tabPressed: ((TGPhotoEditorTab) -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.clipsToBounds = false + self.addSubview(self.buttonsBackgroundView) + + self.selectionView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.15) + self.selectionView.isUserInteractionEnabled = false + self.selectionView.alpha = 0.0 + + self.infoLabel.backgroundColor = .clear + self.infoLabel.textAlignment = .center + self.infoLabel.textColor = .white + self.infoLabel.font = Font.regular(13.0) + self.infoLabel.isUserInteractionEnabled = false + self.addSubview(self.infoLabel) + + self.doneButtonContextView.beginDelay = 0.4 + self.doneButtonContextView.activated = { [weak self] gesture, _ in + guard let self else { + gesture.cancel() + return + } + self.doneLongPressed?(self.doneButtonContextView) + } + self.doneButtonContextView.shouldBegin = { [weak self] _ in + guard let self, let component = self.component else { + return false + } + return self.doneLongPressed != nil && component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden + } + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + fileprivate var cancelButtonFrame: CGRect { + return self.cancelButton.view?.frame ?? .zero + } + + fileprivate var doneButtonFrame: CGRect { + return self.doneButtonContextView.frame + } + + fileprivate var doneButtonSourceView: UIView { + return self.doneButtonContextView + } + + fileprivate func viewForTab(_ tab: TGPhotoEditorTab) -> UIView? { + return self.centerButtonViews[tab.rawValue]?.view + } + + func update(component: MediaPickerPhotoToolbarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.cancelPressed = component.cancelPressed + self.donePressed = component.donePressed + self.doneLongPressed = component.doneLongPressed + self.tabPressed = component.tabPressed + + //let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let accentColor = UIColor(rgb: 0xffd300) //presentationData.theme.list.itemAccentColor + let selectedIconColor = UIColor.white //(rgb: 0xffd300) + + self.backgroundColor = .clear //component.solidBackground ? UIColor(rgb: 0x000000, alpha: 0.55) : .clear + + let cancelSize = self.cancelButton.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: toolbarButtonSide, height: toolbarButtonSide), + backgroundColor: nil, + isDark: true, + state: .glass, + isEnabled: !component.cancelDoneButtonsHidden && !component.allButtonsHidden, + isVisible: true, + component: AnyComponentWithIdentity( + id: "close", + component: AnyComponent( + BundleIconComponent( + name: component.backButtonType == TGPhotoEditorBackButtonBack ? "Navigation/Back" : "Navigation/Close", + tintColor: .white + ) + ) + ), + action: { [weak self] _ in + self?.cancelPressed?() + } + ) + ), + environment: {}, + containerSize: CGSize(width: toolbarButtonSide, height: toolbarButtonSide) + ) + + let doneIconName: String + let doneBackgroundColor: UIColor? = UIColor(rgb: 0x0088ff) + var doneWidth: CGFloat = toolbarButtonSide + switch component.doneButtonType { + case TGPhotoEditorDoneButtonSend: + doneIconName = "Chat/Input/Text/SendIcon" + doneWidth = 46.0 + case TGPhotoEditorDoneButtonSchedule: + doneIconName = "Chat/Input/ScheduleIcon" + doneWidth = 46.0 + case TGPhotoEditorDoneButtonDone: + doneIconName = "Navigation/Done" + default: + doneIconName = "Navigation/Done" + } + + let doneSize = self.doneButton.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: doneWidth, height: toolbarButtonSide), + backgroundColor: doneBackgroundColor, + isDark: true, + state: doneBackgroundColor != nil ? .tintedGlass : .glass, + isEnabled: component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden, + isVisible: true, + component: AnyComponentWithIdentity( + id: "done", + component: AnyComponent( + BundleIconComponent( + name: doneIconName, + tintColor: .white + ) + ) + ), + action: { [weak self] _ in + self?.donePressed?() + } + ) + ), + environment: {}, + containerSize: CGSize(width: doneWidth, height: toolbarButtonSide) + ) + +// let doneContent: AnyComponentWithIdentity +// let doneFixedSize: CGSize? +// let doneAvailableSize: CGSize +// if component.hasSendStarsButton { +// let text = "\u{2b50}\u{fe0f} \(component.sendPaidMessageStars)" +// doneContent = AnyComponentWithIdentity( +// id: "stars-\(component.sendPaidMessageStars)", +// component: AnyComponent(Text(text: text, font: Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers), color: .white)) +// ) +// doneFixedSize = nil +// doneAvailableSize = CGSize(width: 180.0, height: toolbarSideButtonSide) +// } else { +// doneContent = self.sideButtonContent(iconName: self.doneIconName(component.doneButtonType), color: .white, id: "done-\(component.doneButtonType.rawValue)") +// doneFixedSize = CGSize(width: toolbarSideButtonSide, height: toolbarSideButtonSide) +// doneAvailableSize = CGSize(width: toolbarSideButtonSide, height: toolbarSideButtonSide) +// } +// let doneSize = self.doneButton.update( +// content: doneContent, +// fixedSize: doneFixedSize, +// availableSize: doneAvailableSize, +// isEnabled: component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden, +// transition: transition +// ) + + let sideAlpha = component.allButtonsHidden ? 0.0 : 1.0 + let sideFrames = self.sideButtonFrames(availableSize: availableSize, cancelSize: cancelSize, doneSize: doneSize, component: component) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + } + transition.setAlpha(view: cancelButtonView, alpha: sideAlpha) + transition.setFrame(view: cancelButtonView, frame: sideFrames.cancel) + } + if let doneButtonView = self.doneButton.view { + if self.doneButtonContextView.superview == nil { + self.addSubview(self.doneButtonContextView) + } + if doneButtonView.superview !== self.doneButtonContextView { + self.doneButtonContextView.addSubview(doneButtonView) + } + self.doneButtonContextView.targetViewForActivationProgress = doneButtonView + self.doneButtonContextView.isGestureEnabled = component.doneLongPressed != nil && component.doneButtonEnabled && !component.cancelDoneButtonsHidden && !component.allButtonsHidden + transition.setAlpha(view: self.doneButtonContextView, alpha: sideAlpha * (component.doneButtonEnabled ? 1.0 : 0.2)) + transition.setFrame(view: self.doneButtonContextView, frame: sideFrames.done) + transition.setFrame(view: doneButtonView, frame: CGRect(origin: .zero, size: sideFrames.done.size)) + } + + self.updateCenterButtons( + component: component, + availableSize: availableSize, + doneSize: doneSize, + selectedIconColor: selectedIconColor, + accentColor: accentColor, + transition: transition + ) + + self.updateInfoLabel(component: component, availableSize: availableSize, transition: transition) + + return availableSize + } + + private func doneIconName(_ type: TGPhotoEditorDoneButton) -> String { + switch type.rawValue { + case 1: + return "Editor/Commit" + case 2: + return "Editor/Commit" + case 3: + return "PhotoPickerSendIcon" + default: + return "PhotoPickerSendIcon" + } + } + + private func sideButtonFrames(availableSize: CGSize, cancelSize: CGSize, doneSize: CGSize, component: MediaPickerPhotoToolbarComponent) -> (cancel: CGRect, done: CGRect) { + let sideInset: CGFloat = 26.0 + if availableSize.width > availableSize.height { + let cancelFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: cancelSize) + let doneFrame: CGRect + if component.hasSendStarsButton { + doneFrame = CGRect(x: availableSize.width - doneSize.width - 2.0, y: 0.0, width: doneSize.width, height: doneSize.height) + } else { + doneFrame = CGRect(x: availableSize.width - doneSize.width - sideInset, y: 0.0, width: doneSize.width, height: doneSize.height) + } + return (cancelFrame, doneFrame) + } else { + let offset: CGFloat = component.interfaceOrientation == .landscapeLeft ? availableSize.width - toolbarSideButtonSide : 0.0 + let cancelFrame = CGRect(x: offset, y: availableSize.height - toolbarSideButtonSide, width: toolbarSideButtonSide, height: toolbarSideButtonSide) + let doneFrame = CGRect(x: offset, y: 0.0, width: toolbarSideButtonSide, height: toolbarSideButtonSide) + return (cancelFrame, doneFrame) + } + } + + private func updateCenterButtons(component: MediaPickerPhotoToolbarComponent, availableSize: CGSize, doneSize: CGSize, selectedIconColor: UIColor, accentColor: UIColor, transition: ComponentTransition) { + let tabs = toolbarTabOrder.filter { component.currentTabs.contains($0) } + let visibleRawValues = Set(tabs.map { $0.rawValue }) + + for (rawValue, buttonView) in Array(self.centerButtonViews) { + if !visibleRawValues.contains(rawValue) { + if let view = buttonView.view { + transition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + self.centerButtonViews.removeValue(forKey: rawValue) + } + } + + guard !tabs.isEmpty else { + transition.setAlpha(view: self.buttonsBackgroundView, alpha: 0.0) + transition.setAlpha(view: self.selectionView, alpha: 0.0) + self.buttonsBackgroundView.isUserInteractionEnabled = false + return + } + + let buttonFrames = self.centerButtonFrames(tabs: tabs, availableSize: availableSize, doneSize: doneSize, component: component) + var backgroundFrame = CGRect.null + for tab in tabs { + if let frame = buttonFrames[tab.rawValue] { + backgroundFrame = backgroundFrame.union(frame) + } + } + if backgroundFrame.isNull { + transition.setAlpha(view: self.buttonsBackgroundView, alpha: 0.0) + transition.setAlpha(view: self.selectionView, alpha: 0.0) + self.buttonsBackgroundView.isUserInteractionEnabled = false + return + } + backgroundFrame = CGRect( + x: floorToScreenPixels(backgroundFrame.minX), + y: floorToScreenPixels(backgroundFrame.minY), + width: ceil(backgroundFrame.width), + height: ceil(backgroundFrame.height) + ) + + let centerHidden = component.allButtonsHidden || component.centerButtonsHidden || component.editButtonsHidden + let centerAlpha: CGFloat = centerHidden ? 0.0 : (component.editButtonsEnabled ? 1.0 : 0.2) + self.buttonsBackgroundView.isUserInteractionEnabled = !centerHidden && component.editButtonsEnabled + transition.setAlpha(view: self.buttonsBackgroundView, alpha: centerAlpha) + transition.setFrame(view: self.buttonsBackgroundView, frame: backgroundFrame) + + let minSide = min(backgroundFrame.width, backgroundFrame.height) + self.buttonsBackgroundView.update(size: backgroundFrame.size, cornerRadius: minSide * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, isVisible: !centerHidden, transition: transition) + + if self.selectionView.superview == nil { + self.buttonsBackgroundView.contentView.insertSubview(self.selectionView, at: 0) + } else { + self.buttonsBackgroundView.contentView.sendSubviewToBack(self.selectionView) + } + + var selectionFrame: CGRect? + + for tab in tabs { + guard let buttonFrame = buttonFrames[tab.rawValue] else { + continue + } + + let rawValue = tab.rawValue + let buttonView: ComponentView + if let current = self.centerButtonViews[rawValue] { + buttonView = current + } else { + buttonView = ComponentView() + self.centerButtonViews[rawValue] = buttonView + } + + let isDisabled = component.disabledTabs.contains(tab) + let isHighlighted = component.highlightedTabs.contains(tab) + let selectedVisual = component.activeTab == tab && !dontHighlightOnSelectionTabs.contains(rawValue) + let iconColor: UIColor + if selectedVisual { + iconColor = selectedIconColor + } else if isHighlighted && !isDisabled { + iconColor = accentColor + } else { + iconColor = .white + } + + let content: AnyComponent + switch tab { + case .qualityTab: + content = AnyComponent( + Image( + image: self.imageCache.qualityIcon( + isPhoto: component.qualityIsPhoto, + highQuality: component.qualityHighQuality, + preset: component.qualityPreset, + color: iconColor + ), + size: CGSize(width: 28.0, height: 22.0), + contentMode: .center + ) + ) + case .timerTab: + content = AnyComponent( + Image( + image: self.imageCache.timerIcon(value: component.timerValue, color: iconColor), + size: CGSize(width: 24.0, height: 24.0), + contentMode: .center + ) + ) + default: + content = AnyComponent( + BundleIconComponent( + name: self.iconName(tab), + tintColor: iconColor, + maxSize: CGSize(width: 28.0, height: 28.0) + ) + ) + } + let buttonSize = buttonView.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: content, + minSize: CGSize(width: toolbarButtonSide, height: toolbarButtonSide), + action: { [weak self] in + self?.tabPressed?(tab) + }, + isEnabled: component.editButtonsEnabled && !isDisabled, + animateAlpha: true, + animateScale: false, + animateContents: false + ) + ), + environment: {}, + containerSize: CGSize(width: toolbarButtonSide, height: toolbarButtonSide) + ) + + if let view = buttonView.view { + if view.superview == nil { + self.buttonsBackgroundView.contentView.addSubview(view) + } + let localFrame = CGRect( + x: buttonFrame.minX - backgroundFrame.minX, + y: buttonFrame.minY - backgroundFrame.minY, + width: buttonSize.width, + height: buttonSize.height + ) + if selectedVisual { + let selectionSide = max(0.0, min(backgroundFrame.width, backgroundFrame.height) - 6.0) + selectionFrame = CGRect( + x: floorToScreenPixels(localFrame.midX - selectionSide / 2.0), + y: floorToScreenPixels(localFrame.midY - selectionSide / 2.0), + width: selectionSide, + height: selectionSide + ) + } + transition.setFrame(view: view, frame: localFrame) + transition.setAlpha(view: view, alpha: isDisabled ? 0.2 : 1.0) + } + } + + if let selectionFrame = selectionFrame { + self.selectionView.layer.cornerRadius = selectionFrame.width * 0.5 + transition.setFrame(view: self.selectionView, frame: selectionFrame) + transition.setAlpha(view: self.selectionView, alpha: centerHidden ? 0.0 : 1.0) + } else { + transition.setAlpha(view: self.selectionView, alpha: 0.0) + } + } + + private func iconName(_ tab: TGPhotoEditorTab) -> String { + switch tab { + case .cropTab: + return "Media Editor/Crop" + case .toolsTab: + return "Media Editor/Adjustments" + case .rotateTab: + return "Editor/Rotate" + case .paintTab: + return "Media Editor/Pencil" + case .stickerTab: + return "Media Editor/AddSticker" + case .textTab: + return "Media Editor/AddText" + case .eraserTab: + return "Editor/Eraser" + case .mirrorTab: + return "Media Editor/Mirror" + case .aspectRatioTab: + return "Editor/AspectRatio" + case .tintTab: + return "Media Editor/Tint" + case .blurTab: + return "Media Editor/Blur" + case .curvesTab: + return "Media Editor/Curves" + default: + return "Media Editor/Crop" + } + } + + private func centerButtonFrames(tabs: [TGPhotoEditorTab], availableSize: CGSize, doneSize: CGSize, component: MediaPickerPhotoToolbarComponent) -> [UInt: CGRect] { + var result: [UInt: CGRect] = [:] + let count = tabs.count + guard count != 0 else { + return result + } + + let totalLength = CGFloat(count) * toolbarButtonSide + CGFloat(count - 1) * centerButtonSpacing + let step = toolbarButtonSide + centerButtonSpacing + + if availableSize.width > availableSize.height { + let leftEdge = toolbarSideButtonSide + let rightEdge: CGFloat = component.hasSendStarsButton ? doneSize.width + 2.0 : toolbarSideButtonSide + let availableWidth = availableSize.width - leftEdge - rightEdge + let startX = floorToScreenPixels(leftEdge + (availableWidth - totalLength) / 2.0) + + for i in 0 ..< count { + result[tabs[i].rawValue] = CGRect( + x: startX + CGFloat(i) * step, + y: 0.0, + width: toolbarButtonSide, + height: toolbarButtonSide + ) + } + } else { + let x: CGFloat + if component.interfaceOrientation == .landscapeLeft { + x = availableSize.width - toolbarButtonSide - 8.0 + } else { + x = 8.0 + } + + let topInset = toolbarSideButtonSide + let bottomInset = toolbarSideButtonSide + let availableHeight = availableSize.height - topInset - bottomInset + let startY = floorToScreenPixels(topInset + (availableHeight - totalLength) / 2.0) + + for i in 0 ..< count { + result[tabs[i].rawValue] = CGRect( + x: x, + y: startY + CGFloat(i) * step, + width: toolbarButtonSide, + height: toolbarButtonSide + ) + } + } + + return result + } + + private func updateInfoLabel(component: MediaPickerPhotoToolbarComponent, availableSize: CGSize, transition: ComponentTransition) { + self.infoLabel.text = component.infoString + self.infoLabel.isHidden = component.infoString == nil + guard component.infoString != nil else { + return + } + + if availableSize.width > availableSize.height { + self.infoLabel.transform = .identity + transition.setFrame(view: self.infoLabel, frame: CGRect(x: toolbarSideButtonSide + 10.0, y: 0.0, width: availableSize.width - (toolbarSideButtonSide + 10.0) * 2.0, height: toolbarSideButtonSide)) + } else { + let bounds = CGRect(x: 0.0, y: 0.0, width: availableSize.height - (toolbarSideButtonSide + 10.0) * 2.0, height: availableSize.width) + self.infoLabel.bounds = bounds + self.infoLabel.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) + if component.interfaceOrientation == .landscapeLeft { + self.infoLabel.transform = CGAffineTransform(rotationAngle: .pi / 2.0) + } else if component.interfaceOrientation == .landscapeRight { + self.infoLabel.transform = CGAffineTransform(rotationAngle: -.pi / 2.0) + } else { + self.infoLabel.transform = .identity + } + } + } + } + + func makeView() -> View { + return View(frame: .zero) + } + + 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 func makeMediaPickerPhotoToolbarView(context: AccountContext, backButton: TGPhotoEditorBackButton, doneButton: TGPhotoEditorDoneButton, solidBackground: Bool, hasSendStarsButton: Bool) -> (UIView & TGPhotoToolbarViewProtocol)? { + return MediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) +} + +final class MediaPickerPhotoToolbarView: UIView, TGPhotoToolbarViewProtocol { + private let context: AccountContext + private let solidBackground: Bool + private let hasSendStarsButton: Bool + private let rootView = ComponentView() + + private var transitionedOut = false + + var cancelPressed: (() -> Void)? { + didSet { + self.update(transition: .immediate) + } + } + var donePressed: (() -> Void)? { + didSet { + self.update(transition: .immediate) + } + } + var doneLongPressed: ((Any?) -> Void)? { + didSet { + self.update(transition: .immediate) + } + } + var tabPressed: ((TGPhotoEditorTab) -> Void)? { + didSet { + self.update(transition: .immediate) + } + } + + var interfaceOrientation: UIInterfaceOrientation = .portrait { + didSet { + self.update(transition: .immediate) + } + } + + var bottomInset: CGFloat = 0.0 + + var backButtonType: TGPhotoEditorBackButton { + didSet { + self.update(transition: .immediate) + } + } + + var doneButtonType: TGPhotoEditorDoneButton { + didSet { + self.update(transition: .immediate) + } + } + + var sendPaidMessageStars: Int64 = 0 { + didSet { + self.update(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))) + } + } + + private(set) var currentTabs: TGPhotoEditorTab = [] + private var activeTab: TGPhotoEditorTab = [] + private var highlightedTabs: TGPhotoEditorTab = [] + private var disabledTabs: TGPhotoEditorTab = [] + private var editButtonsHidden = false + private var editButtonsEnabled = true + private var centerButtonsHidden = false + private var allButtonsHidden = false + private var cancelDoneButtonsHidden = false + private var doneButtonEnabled = true + private var qualityIsPhoto = false + private var qualityHighQuality = false + private var qualityPreset = 3 + private var timerValue = 0 + private var infoString: String? + + var doneButton: UIView { + return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.doneButtonSourceView ?? UIView() + } + + var cancelButtonFrame: CGRect { + return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.cancelButtonFrame ?? .zero + } + + var doneButtonFrame: CGRect { + return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.doneButtonFrame ?? .zero + } + + init(context: AccountContext, backButton: TGPhotoEditorBackButton, doneButton: TGPhotoEditorDoneButton, solidBackground: Bool, hasSendStarsButton: Bool) { + self.context = context + self.backButtonType = backButton + self.doneButtonType = doneButton + self.solidBackground = solidBackground + self.hasSendStarsButton = hasSendStarsButton + + super.init(frame: .zero) + + self.clipsToBounds = false + self.backgroundColor = .clear + self.update(transition: .immediate) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.update(transition: .immediate) + self.updateRootFrame(transition: .immediate) + } + + func transitionIn(animated: Bool) { + self.transitionIn(animated: animated, transparent: false) + } + + func transitionIn(animated: Bool, transparent: Bool) { + self.transitionedOut = false + self.isHidden = false + if !transparent { + self.backgroundColor = UIColor(rgb: 0x000000) + } + + let transition = animated ? ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)) : .immediate + self.updateRootFrame(transition: transition) + + if !transparent { + self.backgroundColor = .clear + } + } + + func transitionOut(animated: Bool) { + self.transitionOut(animated: animated, transparent: false, hideOnCompletion: false) + } + + func transitionOut(animated: Bool, transparent: Bool, hideOnCompletion: Bool) { + self.transitionedOut = true + if !transparent { + self.backgroundColor = UIColor(rgb: 0x000000) + } + + let duration: Double = animated ? 0.3 : 0.0 + let transition = animated ? ComponentTransition(animation: .curve(duration: duration, curve: .easeInOut)) : .immediate + self.updateRootFrame(transition: transition) + + if hideOnCompletion { + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak self] in + self?.isHidden = true + } + } + } + + func setDoneButtonEnabled(_ enabled: Bool, animated: Bool) { + self.doneButtonEnabled = enabled + self.update(transition: self.transition(animated: animated)) + } + + func setEditButtonsEnabled(_ enabled: Bool, animated: Bool) { + self.editButtonsEnabled = enabled + self.update(transition: self.transition(animated: animated)) + } + + func setEditButtonsHidden(_ hidden: Bool, animated: Bool) { + self.editButtonsHidden = hidden + self.update(transition: self.transition(animated: animated)) + } + + func setEditButtonsHighlighted(_ buttons: TGPhotoEditorTab) { + self.highlightedTabs = buttons + self.update(transition: .immediate) + } + + func setEditButtonsDisabled(_ buttons: TGPhotoEditorTab) { + self.disabledTabs = buttons + self.update(transition: .immediate) + } + + func setCenterButtonsHidden(_ hidden: Bool, animated: Bool) { + self.centerButtonsHidden = hidden + self.update(transition: self.transition(animated: animated)) + } + + func setAllButtonsHidden(_ hidden: Bool, animated: Bool) { + self.allButtonsHidden = hidden + self.update(transition: self.transition(animated: animated)) + } + + func setCancelDoneButtonsHidden(_ hidden: Bool, animated: Bool) { + self.cancelDoneButtonsHidden = hidden + self.update(transition: self.transition(animated: animated)) + } + + func setToolbarTabs(_ tabs: TGPhotoEditorTab, animated: Bool) { + self.currentTabs = tabs + self.update(transition: self.transition(animated: animated)) + } + + func setActiveTab(_ tab: TGPhotoEditorTab) { + self.activeTab = tab + self.update(transition: .spring(duration: 0.4)) + } + + func setQualityButtonIsPhoto(_ isPhoto: Bool, highQuality: Bool, videoPreset: Int) { + self.qualityIsPhoto = isPhoto + self.qualityHighQuality = highQuality + self.qualityPreset = videoPreset + self.update(transition: .immediate) + } + + func setTimerButtonValue(_ value: Int) { + self.timerValue = value + self.update(transition: .immediate) + } + + func setInfoString(_ string: String?) { + self.infoString = string + self.update(transition: .immediate) + } + + @objc(viewForTab:) + func view(for tab: TGPhotoEditorTab) -> UIView? { + return (self.rootView.view as? MediaPickerPhotoToolbarComponent.View)?.viewForTab(tab) + } + + private func transition(animated: Bool) -> ComponentTransition { + if animated { + return ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } else { + return .immediate + } + } + + private func update(transition: ComponentTransition) { + let size = self.bounds.size + let _ = self.rootView.update( + transition: transition, + component: AnyComponent( + MediaPickerPhotoToolbarComponent( + context: self.context, + solidBackground: self.solidBackground, + backButtonType: self.backButtonType, + doneButtonType: self.doneButtonType, + currentTabs: self.currentTabs, + activeTab: self.activeTab, + highlightedTabs: self.highlightedTabs, + disabledTabs: self.disabledTabs, + qualityIsPhoto: self.qualityIsPhoto, + qualityHighQuality: self.qualityHighQuality, + qualityPreset: self.qualityPreset, + timerValue: self.timerValue, + hasSendStarsButton: self.hasSendStarsButton, + sendPaidMessageStars: self.sendPaidMessageStars, + editButtonsHidden: self.editButtonsHidden, + editButtonsEnabled: self.editButtonsEnabled, + centerButtonsHidden: self.centerButtonsHidden, + allButtonsHidden: self.allButtonsHidden, + cancelDoneButtonsHidden: self.cancelDoneButtonsHidden, + doneButtonEnabled: self.doneButtonEnabled, + interfaceOrientation: self.interfaceOrientation, + bottomInset: self.bottomInset, + infoString: self.infoString, + cancelPressed: self.cancelPressed, + donePressed: self.donePressed, + doneLongPressed: self.doneLongPressed, + tabPressed: self.tabPressed + ) + ), + environment: {}, + forceUpdate: true, + containerSize: size + ) + + if let view = self.rootView.view { + if view.superview == nil { + self.addSubview(view) + } + self.updateRootFrame(transition: transition) + } + } + + private func updateRootFrame(transition: ComponentTransition) { + guard let view = self.rootView.view else { + return + } + + var frame = CGRect(origin: .zero, size: self.bounds.size) + if self.transitionedOut { + if self.bounds.width > self.bounds.height { + frame.origin.y = self.bounds.height + } else if self.interfaceOrientation == .landscapeLeft { + frame.origin.x = -self.bounds.width + } else { + frame.origin.x = self.bounds.width + } + } + transition.setFrame(view: view, frame: frame) + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 142ad3d7dc..a95317f79d 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -15,7 +15,6 @@ import LegacyComponents import LegacyMediaPickerUI import AttachmentUI import ContextUI -import WebSearchUI import SparseItemGrid import UndoUI import PresentationDataUtils @@ -192,7 +191,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att case addImage case cover case createSticker - case createAvatar + case createAvatar(mode: PeerType) case poll(mode: PollMode, asFile: Bool) } @@ -244,12 +243,9 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att private let moreButtonNode: MoreButtonNode private let selectedButtonNode: SelectedButtonNode - public weak var webSearchController: WebSearchController? - public var openCamera: ((Any?) -> Void)? public var presentSchedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in } public var presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in } - public var presentWebSearch: (MediaGroupsScreen, Bool) -> Void = { _, _ in } public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } public var openBoost: () -> Void = { } @@ -705,8 +701,13 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att var useModernCamera = false if case .assets(nil, .default) = controller.subject { useLegacyCamera = true - } else if case .assets(nil, let mode) = controller.subject, [.createSticker, .createAvatar].contains(mode) { - useModernCamera = true + } else if case .assets(nil, let mode) = controller.subject { + switch mode { + case .createSticker, .createAvatar: + useModernCamera = true + default: + break + } } if useLegacyCamera { @@ -1517,28 +1518,32 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } } - private func openLimitedMediaOptions() { - let presentationData = self.presentationData - let controller = ActionSheetController(presentationData: self.presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() + private func openLimitedMediaOptions(sourceView: UIView) { + guard let controller = self.controller else { + return } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Media_LimitedAccessSelectMore, color: .accent, action: { [weak self] in - dismissAction() - if #available(iOS 14.0, *), let strongController = self?.controller { - PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: strongController) - } - }), - ActionSheetButtonItem(title: presentationData.strings.Media_LimitedAccessChangeSettings, color: .accent, action: { [weak self] in - dismissAction() - self?.controller?.context.sharedContext.applicationBindings.openSettings() - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - self.controller?.present(controller, in: .window(.root)) + + let presentationData = self.presentationData + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: presentationData.strings.Media_LimitedAccessSelectMore, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Image"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + if #available(iOS 14.0, *), let controller = self?.controller { + PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: controller) + } + })), + .action(ContextMenuActionItem(text: presentationData.strings.Media_LimitedAccessChangeSettings, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + self?.controller?.context.sharedContext.applicationBindings.openSettings() + })) + ] + let contextController = makeContextController(presentationData: presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + controller.presentInGlobalOverlay(contextController) } private func getItemSnapshot(_ identifier: String) -> UIView? { @@ -1770,9 +1775,9 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att manageNode = current } else { manageNode = MediaPickerManageNode() - manageNode.pressed = { [weak self] in + manageNode.pressed = { [weak self] sourceView in if let strongSelf = self { - strongSelf.openLimitedMediaOptions() + strongSelf.openLimitedMediaOptions(sourceView: sourceView) } } self.manageNode = manageNode @@ -1788,7 +1793,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att manageNode = current } else { manageNode = MediaPickerManageNode() - manageNode.pressed = { [weak self] in + manageNode.pressed = { [weak self] _ in self?.controller?.context.sharedContext.applicationBindings.openSettings() } self.manageNode = manageNode @@ -2070,9 +2075,16 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.titleView.title = presentationData.strings.MediaPicker_Recents self.titleView.subtitle = presentationData.strings.MediaPicker_CreateSticker self.titleView.isEnabled = true - case .createAvatar: + case let .createAvatar(avatarMode): self.titleView.title = presentationData.strings.MediaPicker_Recents - self.titleView.subtitle = presentationData.strings.MediaPicker_SetNewPhoto + switch avatarMode { + case .user: + self.titleView.subtitle = presentationData.strings.MediaPicker_SetNewPhoto + case .group: + self.titleView.subtitle = presentationData.strings.MediaPicker_SetNewGroupPhoto + case .channel: + self.titleView.subtitle = presentationData.strings.MediaPicker_SetNewChannelPhoto + } self.titleView.isEnabled = true case .story: self.titleView.title = presentationData.strings.MediaPicker_Recents @@ -2209,21 +2221,13 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.scrollToTop = { [weak self] in if let strongSelf = self { - if let webSearchController = strongSelf.webSearchController { - webSearchController.scrollToTop?() - } else { - strongSelf.controllerNode.scrollToTop(animated: true) - } + strongSelf.controllerNode.scrollToTop(animated: true) } } self.scrollToTopWithTabBar = { [weak self] in if let strongSelf = self { - if let webSearchController = strongSelf.webSearchController { - webSearchController.cancel() - } else { - strongSelf.scrollToTop?() - } + strongSelf.scrollToTop?() } } @@ -2822,7 +2826,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.updateNavigationStack { current in var mediaPickerContext: AttachmentMediaPickerContext? if let first = current.first as? MediaPickerScreenImpl { - mediaPickerContext = first.webSearchController?.mediaPickerContext ?? first.mediaPickerContext + mediaPickerContext = first.mediaPickerContext } return (current.filter { $0 !== self }, mediaPickerContext) } @@ -2905,10 +2909,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } public func resetForReuse() { - if let webSearchController = self.webSearchController { - self.webSearchController = nil - webSearchController.dismiss() - } self.scrollToTop?() self.controllerNode.isSuspended = true @@ -2971,8 +2971,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att if case .story = mode { self.updateNavigationStack({ _ in return ([self, groupsController], self.mediaPickerContext)}) - } else { - self.presentWebSearch(groupsController, activateOnDisplay) } self.groupsController = groupsController } @@ -3589,26 +3587,6 @@ public func mediaPickerController( completion(result) controller.dismiss(animated: true) } - if hasSearch { - mediaPickerController.presentWebSearch = { [weak mediaPickerController] groups, activateOnDisplay in - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - |> deliverOnMainQueue).start(next: { configuration in - let webSearchController = WebSearchController( - context: context, - updatedPresentationData: updatedPresentationData, - peer: nil, - chatLocation: nil, - configuration: configuration, - mode: .editor(completion: { [weak mediaPickerController] image in - completion(image) - mediaPickerController?.dismiss(animated: true) - }), - activateOnDisplay: activateOnDisplay - ) - mediaPickerController?.present(webSearchController, in: .current) - }) - } - } present(mediaPickerController, mediaPickerController.mediaPickerContext) return true } @@ -3909,6 +3887,7 @@ public func stickerMediaPickerController( public func avatarMediaPickerController( context: AccountContext, + peerType: PeerType, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, @@ -4022,7 +4001,7 @@ public func avatarMediaPickerController( chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, - subject: .assets(nil, .createAvatar), + subject: .assets(nil, .createAvatar(mode: peerType)), mainButtonState: mainButtonState, mainButtonAction: { [weak controller] in controller?.dismiss(animated: true) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift index 31622e2b02..2f58275139 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerSelectedListNode.swift @@ -663,6 +663,7 @@ final class MediaPickerSelectedListNode: ASDisplayNode, ASScrollViewDelegate, AS self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.panGestureRecognizer.cancelsTouchesInView = true self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false self.view.addGestureRecognizer(ReorderingGestureRecognizer(animateOnTouch: !self.persistentItems, shouldBegin: { [weak self] point in if let strongSelf = self, !strongSelf.scrollNode.view.isDragging && strongSelf.itemNodes.count > 1 { diff --git a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift index 680b6eb9ee..656f3c7938 100644 --- a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift +++ b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift @@ -405,11 +405,11 @@ public func notificationSoundSelectionController(context: AccountContext, update let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let signal = combineLatest(presentationData, statePromise.get(), context.engine.peers.notificationSoundList()) |> map { presentationData, state, notificationSoundList -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { arguments.cancel() }) - let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + let rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { arguments.complete() }) diff --git a/submodules/OpenInExternalAppUI/BUILD b/submodules/OpenInExternalAppUI/BUILD index ea1b5232ae..7d78a66292 100644 --- a/submodules/OpenInExternalAppUI/BUILD +++ b/submodules/OpenInExternalAppUI/BUILD @@ -11,14 +11,19 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/ComponentFlow:ComponentFlow", "//submodules/Display:Display", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/PhotoResources:PhotoResources", "//submodules/UrlEscaping:UrlEscaping", "//submodules/AppBundle:AppBundle", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/SheetComponent", + "//submodules/Components/ViewControllerComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift b/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift deleted file mode 100644 index f6771facb0..0000000000 --- a/submodules/OpenInExternalAppUI/Sources/OpenInActionSheetController.swift +++ /dev/null @@ -1,264 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import MapKit -import TelegramPresentationData -import AccountContext -import PhotoResources -import AppBundle - -public struct OpenInControllerAction { - public let title: String - public let action: () -> Void - - public init(title: String, action: @escaping () -> Void) { - self.title = title - self.action = action - } -} - -public final class OpenInActionSheetController: ActionSheetController { - private var presentationDisposable: Disposable? - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, item: OpenInItem, additionalAction: OpenInControllerAction? = nil, openUrl: @escaping (String) -> Void) { - var presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - if let forceTheme = forceTheme { - presentationData = presentationData.withUpdated(theme: forceTheme) - } - - let strings = presentationData.strings - - super.init(theme: ActionSheetControllerTheme(presentationData: presentationData)) - - self.presentationDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak self] presentationData in - if let strongSelf = self { - var presentationData = presentationData - if let forceTheme = forceTheme { - presentationData = presentationData.withUpdated(theme: forceTheme) - } - strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData) - } - }) - - self._ready.set(.single(true)) - - let invokeActionImpl: (OpenInAction) -> Void = { action in - switch action { - case let .openUrl(url): - openUrl(url) - case let .openLocation(latitude, longitude, directions): - let placemark = MKPlacemark(coordinate: CLLocationCoordinate2DMake(latitude, longitude), addressDictionary: [:]) - let mapItem = MKMapItem(placemark: placemark) - - if let directions = directions { - let options = [ MKLaunchOptionsDirectionsModeKey: directions.launchOptions ] - MKMapItem.openMaps(with: [MKMapItem.forCurrentLocation(), mapItem], launchOptions: options) - } else { - mapItem.openInMaps(launchOptions: nil) - } - default: - break - } - } - - var items: [ActionSheetItem] = [] - items.append(OpenInActionSheetItem(context: context, strings: strings, options: availableOpenInOptions(context: context, item: item), invokeAction: invokeActionImpl)) - - if let action = additionalAction { - items.append(ActionSheetButtonItem(title: action.title, action: { [weak self] in - action.action() - self?.dismissAnimated() - })) - } - - 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 OpenInActionSheetItem: ActionSheetItem { - let context: AccountContext - let strings: PresentationStrings - let options: [OpenInOption] - let invokeAction: (OpenInAction) -> Void - - init(context: AccountContext, strings: PresentationStrings, options: [OpenInOption], invokeAction: @escaping (OpenInAction) -> Void) { - self.context = context - self.strings = strings - self.options = options - self.invokeAction = invokeAction - } - - func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return OpenInActionSheetItemNode(context: self.context, theme: theme, strings: self.strings, options: self.options, invokeAction: self.invokeAction) - } - - func updateNode(_ node: ActionSheetItemNode) { - } -} - -private final class OpenInActionSheetItemNode: ActionSheetItemNode { - let theme: ActionSheetControllerTheme - let strings: PresentationStrings - - let titleNode: ASTextNode - let scrollNode: ASScrollNode - - let openInNodes: [OpenInAppNode] - - init(context: AccountContext, theme: ActionSheetControllerTheme, strings: PresentationStrings, options: [OpenInOption], invokeAction: @escaping (OpenInAction) -> Void) { - self.theme = theme - self.strings = strings - - let titleFont = Font.medium(floor(theme.baseFontSize * 20.0 / 17.0)) - - self.titleNode = ASTextNode() - self.titleNode.isUserInteractionEnabled = false - self.titleNode.displaysAsynchronously = true - self.titleNode.attributedText = NSAttributedString(string: strings.Map_OpenIn, font: titleFont, textColor: theme.primaryTextColor, paragraphAlignment: .center) - - self.scrollNode = ASScrollNode() - self.scrollNode.view.showsVerticalScrollIndicator = false - self.scrollNode.view.showsHorizontalScrollIndicator = false - self.scrollNode.view.clipsToBounds = false - self.scrollNode.view.scrollsToTop = false - self.scrollNode.view.delaysContentTouches = false - self.scrollNode.scrollableDirections = [.left, .right] - - self.openInNodes = options.map { option in - let node = OpenInAppNode() - node.setup(context: context, theme: theme, option: option, invokeAction: invokeAction) - return node - } - - super.init(theme: theme) - - self.addSubnode(self.titleNode) - - if !self.openInNodes.isEmpty { - for openInNode in openInNodes { - self.scrollNode.addSubnode(openInNode) - } - self.addSubnode(self.scrollNode) - } - } - - public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { - let size = CGSize(width: constrainedSize.width, height: 148.0) - - let titleSize = self.titleNode.measure(size) - self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 16.0), size: CGSize(width: size.width, height: titleSize.height)) - - self.scrollNode.frame = CGRect(origin: CGPoint(x: 0, y: 36.0), size: CGSize(width: size.width, height: size.height - 36.0)) - - let nodeInset: CGFloat = 2.0 - let nodeSize = CGSize(width: 80.0, height: 112.0) - var nodeOffset = nodeInset - - for node in self.openInNodes { - node.frame = CGRect(origin: CGPoint(x: nodeOffset, y: 0.0), size: nodeSize) - nodeOffset += nodeSize.width - } - - if let lastNode = self.openInNodes.last { - let contentSize = CGSize(width: lastNode.frame.maxX + nodeInset, height: self.scrollNode.frame.height) - if self.scrollNode.view.contentSize != contentSize { - self.scrollNode.view.contentSize = contentSize - } - } - - self.updateInternalLayout(size, constrainedSize: constrainedSize) - return size - } -} - -private final class OpenInAppNode : ASDisplayNode { - private let iconNode: TransformImageNode - private let textNode: ASTextNode - private var action: (() -> Void)? - - override init() { - self.iconNode = TransformImageNode() - self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 60.0, height: 60.0)) - self.iconNode.isLayerBacked = true - - self.textNode = ASTextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = true - - super.init() - - self.addSubnode(self.iconNode) - self.addSubnode(self.textNode) - } - - func setup(context: AccountContext, theme: ActionSheetControllerTheme, option: OpenInOption, invokeAction: @escaping (OpenInAction) -> Void) { - let textFont = Font.regular(floor(theme.baseFontSize * 11.0 / 17.0)) - self.textNode.attributedText = NSAttributedString(string: option.title, font: textFont, textColor: theme.primaryTextColor, paragraphAlignment: .center) - - let iconSize = CGSize(width: 60.0, height: 60.0) - let makeLayout = self.iconNode.asyncLayout() - let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets())) - applyLayout() - - switch option.application { - case .safari: - if let image = UIImage(bundleImageName: "Open In/Safari") { - self.iconNode.setSignal(openInAppIcon(engine: context.engine, appIcon: .image(image: image))) - } - case .maps: - if let image = UIImage(bundleImageName: "Open In/Maps") { - self.iconNode.setSignal(openInAppIcon(engine: context.engine, appIcon: .image(image: image))) - } - case let .other(_, identifier, _, store): - self.iconNode.setSignal(openInAppIcon(engine: context.engine, appIcon: .resource(resource: OpenInAppIconResource(appStoreId: identifier, store: store)))) - } - - self.action = { - invokeAction(option.action()) - } - } - - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.action?() - } - } - - override func layout() { - super.layout() - - let bounds = self.bounds - - self.iconNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: CGSize(width: 60.0, height: 60.0)) - self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 14.0 + 60.0 + 4.0), size: CGSize(width: bounds.size.width, height: 16.0)) - } -} diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInAppIconResources.swift b/submodules/OpenInExternalAppUI/Sources/OpenInAppIconResources.swift index 9af464ac9d..1fc779008e 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInAppIconResources.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInAppIconResources.swift @@ -3,6 +3,7 @@ import UIKit import TelegramCore import SwiftSignalKit import Display +import TelegramPresentationData public struct OpenInAppIconResourceId { public let appStoreId: Int64 @@ -131,7 +132,7 @@ private func drawOpenInAppIconBorder(into c: CGContext, arguments: TransformImag c.strokePath() } -public func openInAppIcon(engine: TelegramEngine, appIcon: OpenInAppIcon) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func openInAppIcon(engine: TelegramEngine, appIcon: OpenInAppIcon, withChrome: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { switch appIcon { case let .resource(resource): return openInAppIconData(engine: engine, appIcon: resource) |> map { data in @@ -166,14 +167,17 @@ public func openInAppIcon(engine: TelegramEngine, appIcon: OpenInAppIcon) -> Sig guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } - + context.withFlippedContext { c in c.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: arguments.drawingSize)) - drawOpenInAppIconBorder(into: c, arguments: arguments) + if withChrome { + drawOpenInAppIconBorder(into: c, arguments: arguments) + } } - addCorners(context, arguments: arguments) - + if withChrome { + addCorners(context, arguments: arguments) + } return context }) } diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index 7721f5bdb1..dc427f26f2 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -90,24 +90,20 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope var options: [OpenInOption] = [] switch item { case let .url(url): - var skipSafari = false if url.contains("youtube.com/") || url.contains("youtu.be/") { let updatedUrl = url.replacingOccurrences(of: "https://", with: "youtube://").replacingOccurrences(of: "http://", with: "youtube://") options.append(OpenInOption(identifier: "youtube", application: .other(title: "YouTube", identifier: 544007664, scheme: "youtube", store: nil), action: { return .openUrl(url: updatedUrl) })) - skipSafari = true } - if !skipSafari { - options.append(OpenInOption(identifier: "safari", application: .safari, action: { - var url = url - if url.hasPrefix("https://") { - url = url.replacingOccurrences(of: "https://", with: "x-safari-https://") - } - return .openUrl(url: url) - })) - } + options.append(OpenInOption(identifier: "safari", application: .safari, action: { + var url = url + if url.hasPrefix("https://") { + url = url.replacingOccurrences(of: "https://", with: "x-safari-https://") + } + return .openUrl(url: url) + })) options.append(OpenInOption(identifier: "chrome", application: .other(title: "Chrome", identifier: 535886823, scheme: "googlechrome", store: nil), action: { if let url = URL(string: url), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) { @@ -202,6 +198,10 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope options.append(OpenInOption(identifier: "alook", application: .other(title: "Alook Browser", identifier: 1261944766, scheme: "alook", store: nil), action: { return .openUrl(url: "alook://\(url)") })) + + options.append(OpenInOption(identifier: "vivaldi", application: .other(title: "Vivaldi", identifier: 1633234600, scheme: "vivaldi", store: "us"), action: { + return .openUrl(url: "vivaldi://\(url)") + })) case let .location(location, directions): let lat = location.latitude let lon = location.longitude @@ -334,6 +334,14 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope return .openUrl(url: url) } })) + + options.append(OpenInOption(identifier: "yandexGo", application: .other(title: "Yandex Go", identifier: 472650686, scheme: "yandextaxi", store: nil), action: { + return .openUrl(url: "yandextaxi://route?end-lat=\(lat)&end-lon=\(lon)") + })) + + options.append(OpenInOption(identifier: "yango", application: .other(title: "Yango", identifier: 1437157286, scheme: "yangoride", store: nil), action: { + return .openUrl(url: "yangoride://route?end-lat=\(lat)&end-lon=\(lon)") + })) } return options } diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift new file mode 100644 index 0000000000..d740cba975 --- /dev/null +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptionsScreen.swift @@ -0,0 +1,544 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import MapKit +import TelegramPresentationData +import AccountContext +import AppBundle +import ViewControllerComponent +import SheetComponent +import ButtonComponent +import GlassBarButtonComponent +import BundleIconComponent +import MultilineTextComponent + +public struct OpenInControllerAction { + public let title: String + public let action: () -> Void + + public init(title: String, action: @escaping () -> Void) { + self.title = title + self.action = action + } +} + +public final class OpenInOptionsScreen: ViewControllerComponentContainer { + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + forceTheme: PresentationTheme? = nil, + item: OpenInItem, + additionalAction: OpenInControllerAction? = nil, + openUrl: @escaping (String) -> Void + ) { + let invokeAction: (OpenInAction) -> Void = { action in + switch action { + case let .openUrl(url): + openUrl(url) + case let .openLocation(latitude, longitude, directions): + let placemark = MKPlacemark(coordinate: CLLocationCoordinate2DMake(latitude, longitude), addressDictionary: [:]) + let mapItem = MKMapItem(placemark: placemark) + + if let directions = directions { + let options = [MKLaunchOptionsDirectionsModeKey: directions.launchOptions] + MKMapItem.openMaps(with: [MKMapItem.forCurrentLocation(), mapItem], launchOptions: options) + } else { + mapItem.openInMaps(launchOptions: nil) + } + default: + break + } + } + + let effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal)? + if let forceTheme { + let initial = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + let signal = updatedPresentationData?.signal ?? context.sharedContext.presentationData + effectiveUpdatedPresentationData = ( + initial: initial.withUpdated(theme: forceTheme), + signal: signal |> map { presentationData in + presentationData.withUpdated(theme: forceTheme) + } + ) + } else { + effectiveUpdatedPresentationData = updatedPresentationData + } + + super.init( + context: context, + component: OpenInOptionsScreenComponent( + context: context, + options: availableOpenInOptions(context: context, item: item), + additionalAction: additionalAction, + invokeAction: invokeAction + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + updatedPresentationData: effectiveUpdatedPresentationData + ) + + self.blocksBackgroundWhenInOverlay = true + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class OpenInOptionsScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let options: [OpenInOption] + let additionalAction: OpenInControllerAction? + let invokeAction: (OpenInAction) -> Void + + init( + context: AccountContext, + options: [OpenInOption], + additionalAction: OpenInControllerAction?, + invokeAction: @escaping (OpenInAction) -> Void + ) { + self.context = context + self.options = options + self.additionalAction = additionalAction + self.invokeAction = invokeAction + } + + static func ==(lhs: OpenInOptionsScreenComponent, rhs: OpenInOptionsScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.options.map(\.identifier) != rhs.options.map(\.identifier) { + return false + } + if lhs.additionalAction?.title != rhs.additionalAction?.title { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + 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(animated: Bool) { + guard let controller = self.environment?.controller() else { + return + } + + if animated { + self.sheetAnimateOut.invoke(Action { _ in + controller.dismiss(completion: nil) + }) + } else { + controller.dismiss(animated: false, completion: nil) + } + } + + func update(component: OpenInOptionsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] animated in + self?.dismiss(animated: animated) + } + ) + + let _ = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(OpenInOptionsSheetContentComponent( + context: component.context, + options: component.options, + additionalAction: component.additionalAction, + invokeAction: component.invokeAction, + dismiss: { [weak self] in + self?.dismiss(animated: true) + } + )), + style: .glass, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: self.sheetAnimateOut + )), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + return availableSize + } + } + + 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 OpenInOptionsSheetContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let options: [OpenInOption] + let additionalAction: OpenInControllerAction? + let invokeAction: (OpenInAction) -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + options: [OpenInOption], + additionalAction: OpenInControllerAction?, + invokeAction: @escaping (OpenInAction) -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.options = options + self.additionalAction = additionalAction + self.invokeAction = invokeAction + self.dismiss = dismiss + } + + static func ==(lhs: OpenInOptionsSheetContentComponent, rhs: OpenInOptionsSheetContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.options.map(\.identifier) != rhs.options.map(\.identifier) { + return false + } + if lhs.additionalAction?.title != rhs.additionalAction?.title { + return false + } + return true + } + + final class View: UIView { + private let closeButton = ComponentView() + private let title = ComponentView() + private let scrollView: UIScrollView + private var optionViews: [String: OpenInAppView] = [:] + private var shareButton: ComponentView? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.clipsToBounds = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.alwaysBounceHorizontal = true + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + + super.init(frame: frame) + + self.addSubview(self.scrollView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: OpenInOptionsSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let theme = environment.theme + let strings = environment.strings + + let closeButtonSize = self.closeButton.update( + transition: transition, + component: 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 + component.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.addSubview(closeButtonView) + } + 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(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Map_OpenIn, + font: Font.semibold(17.0), + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: max(1.0, availableSize.width - 32.0 - 60.0), height: CGFloat.greatestFiniteMagnitude) + ) + 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(38.0 - titleSize.height / 2.0) + ), + size: titleSize + )) + } + + let optionInset: CGFloat = 2.0 + var optionSpacing: CGFloat = 8.0 + if component.options.count == 3 { + optionSpacing = 32.0 + } else if component.options.count == 2 { + optionSpacing = 64.0 + } + + let optionSize = CGSize(width: 80.0, height: 112.0) + let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: 82.0), size: CGSize(width: availableSize.width, height: optionSize.height)) + transition.setFrame(view: self.scrollView, frame: scrollFrame) + + var validIds = Set() + for (index, option) in component.options.enumerated() { + validIds.insert(option.identifier) + + let optionView: OpenInAppView + if let current = self.optionViews[option.identifier] { + optionView = current + } else { + optionView = OpenInAppView() + self.optionViews[option.identifier] = optionView + self.scrollView.addSubview(optionView) + } + + optionView.update(context: component.context, theme: theme, option: option, action: { + component.invokeAction(option.action()) + component.dismiss() + }) + + let optionOriginX: CGFloat + if component.options.count < 5 { + optionOriginX = floorToScreenPixels(max(0.0, (availableSize.width - optionSize.width * CGFloat(component.options.count) - optionSpacing * CGFloat(component.options.count - 1)) / 2.0)) + CGFloat(index) * (optionSize.width + optionSpacing) + } else { + optionOriginX = optionInset + CGFloat(index) * (optionSize.width + optionSpacing) + } + + transition.setFrame(view: optionView, frame: CGRect( + origin: CGPoint(x: optionOriginX, y: 0.0), + size: optionSize + )) + } + + for id in Array(self.optionViews.keys) { + if !validIds.contains(id) { + let optionView = self.optionViews.removeValue(forKey: id) + optionView?.removeFromSuperview() + } + } + + let optionsContentWidth: CGFloat + if component.options.isEmpty { + optionsContentWidth = availableSize.width + } else { + optionsContentWidth = optionInset * 2.0 + CGFloat(component.options.count) * (optionSize.width + optionSpacing) - optionSpacing + } + self.scrollView.contentSize = CGSize(width: max(availableSize.width, optionsContentWidth), height: optionSize.height) + + var contentHeight = scrollFrame.maxY + 22.0 + if let additionalAction = component.additionalAction { + let shareButton: ComponentView + if let current = self.shareButton { + shareButton = current + } else { + shareButton = ComponentView() + self.shareButton = shareButton + } + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let buttonSize = shareButton.update( + transition: transition, + component: 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(additionalAction.title), + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: additionalAction.title, + font: Font.semibold(17.0), + textColor: theme.list.itemCheckColors.foregroundColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + )) + ), + action: { + additionalAction.action() + component.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) + ) + + if let shareButtonView = shareButton.view { + if shareButtonView.superview == nil { + self.addSubview(shareButtonView) + } + transition.setFrame(view: shareButtonView, frame: CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: contentHeight), + size: buttonSize + )) + } + contentHeight += buttonSize.height + contentHeight += buttonInsets.bottom + } else { + if let shareButton = self.shareButton { + self.shareButton = nil + shareButton.view?.removeFromSuperview() + } + contentHeight += max(20.0, environment.safeInsets.bottom + 12.0) + } + + 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 OpenInAppView: UIControl { + private let iconView: TransformImageView + private let titleLabel: UILabel + + private var currentIdentifier: String? + private var action: (() -> Void)? + + override init(frame: CGRect) { + self.iconView = TransformImageView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 64.0, height: 64.0))) + self.titleLabel = UILabel() + + super.init(frame: frame) + + self.titleLabel.textAlignment = .center + self.titleLabel.numberOfLines = 1 + self.titleLabel.adjustsFontSizeToFitWidth = true + self.titleLabel.minimumScaleFactor = 0.75 + + self.isAccessibilityElement = true + + self.addSubview(self.iconView) + self.addSubview(self.titleLabel) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(context: AccountContext, theme: PresentationTheme, option: OpenInOption, action: @escaping () -> Void) { + self.action = action + self.accessibilityLabel = option.title + self.titleLabel.attributedText = NSAttributedString(string: option.title, font: Font.medium(12.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) + + let iconSize = CGSize(width: 64.0, height: 64.0) + let makeLayout = self.iconView.asyncLayout() + let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets())) + applyLayout() + + if self.currentIdentifier != option.identifier { + self.currentIdentifier = option.identifier + + switch option.application { + case .safari: + if let image = UIImage(bundleImageName: "Open In/Safari") { + self.iconView.setSignal(openInAppIcon(engine: context.engine, appIcon: .image(image: image))) + } + case .maps: + if let image = UIImage(bundleImageName: "Open In/Maps") { + self.iconView.setSignal(openInAppIcon(engine: context.engine, appIcon: .image(image: image))) + } + case let .other(_, identifier, _, store): + self.iconView.setSignal(openInAppIcon(engine: context.engine, appIcon: .resource(resource: OpenInAppIconResource(appStoreId: identifier, store: store)))) + } + } + } + + @objc private func pressed() { + self.action?() + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.iconView.frame = CGRect(origin: CGPoint(x: 8.0, y: 12.0), size: CGSize(width: 64.0, height: 64.0)) + self.titleLabel.frame = CGRect(origin: CGPoint(x: 0.0, y: 80.0), size: CGSize(width: self.bounds.width, height: 16.0)) + } +} 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/SecureIdAuthControllerNode.swift b/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift index cc9c224885..ee691a9d42 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift @@ -51,6 +51,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { self.addSubnode(self.activityIndicator) self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) self.backgroundColor = presentationData.theme.list.blocksBackgroundColor 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 deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) var rightNavigationButton: ItemListNavigationButton? if state.checking { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.code.isEmpty, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: !state.code.isEmpty, action: { var state: ResetPasswordControllerState? updateState { s in state = s diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index cf77e35974..aea80e2cf4 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -1819,6 +1819,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, ASS self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/submodules/PeerInfoUI/BUILD b/submodules/PeerInfoUI/BUILD index 5a25b9755f..0ee49ffed5 100644 --- a/submodules/PeerInfoUI/BUILD +++ b/submodules/PeerInfoUI/BUILD @@ -34,7 +34,6 @@ swift_library( "//submodules/LegacyUI:LegacyUI", "//submodules/GalleryUI:GalleryUI", "//submodules/ShareController:ShareController", - "//submodules/WebSearchUI:WebSearchUI", "//submodules/SearchBarNode:SearchBarNode", "//submodules/ContactsPeerItem:ContactsPeerItem", "//submodules/PasswordSetupUI:PasswordSetupUI", @@ -80,10 +79,14 @@ swift_library( "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", "//submodules/TelegramUI/Components/PeerInfo/MessagePriceItem", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", + "//submodules/TelegramUI/Components/AvatarComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/ComponentFlow", "//submodules/Components/ComponentDisplayAdapters", "//submodules/CounterControllerTitleView", + "//submodules/TelegramUI/Components/ChatTimerScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift index 10bf515648..dc1bce65f1 100644 --- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift +++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift @@ -164,7 +164,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let text: String text = presentationData.strings.Login_UnknownError - baseController?.present(textAlertController(context: strongSelf.context, updatedPresentationData: nil, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + baseController?.present(textAlertController(context: strongSelf.context, forceTheme: defaultDarkPresentationTheme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) })) } @@ -191,17 +191,6 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let credentialsSection = Child(ListSectionComponent.self) -// let credentialsBackground = Child(RoundedRectangle.self) -// let credentialsStripe = Child(Rectangle.self) -// let credentialsURLTitle = Child(MultilineTextComponent.self) -// let credentialsURLText = Child(MultilineTextComponent.self) -// -// let credentialsKeyTitle = Child(MultilineTextComponent.self) -// let credentialsKeyText = Child(MultilineTextComponent.self) -// -// let credentialsCopyURLButton = Child(Button.self) -// let credentialsCopyKeyButton = Child(Button.self) - return { context in let topInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0 @@ -477,12 +466,13 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent maximumNumberOfLines: 1 )), titleAlignment: .center, + accessory: .none, action: { [weak state] _ in guard let state = state else { return } - let alertController = textAlertController(context: component.context, title: nil, text: environment.strings.CreateExternalStream_Revoke_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { - }), TextAlertAction(type: .defaultAction, title: environment.strings.CreateExternalStream_Revoke_Revoke, action: { [weak state] in + let alertController = textAlertController(context: component.context, forceTheme: theme, title: nil, text: environment.strings.CreateExternalStream_Revoke_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultDestructiveAction, title: environment.strings.CreateExternalStream_Revoke_Revoke, action: { [weak state] in state?.getCredentials(revoke: true) })]) environment.controller()?.present(alertController, in: .window(.root)) diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index ab6d4cfe48..4967801ddb 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -16,6 +16,10 @@ import Markdown import SendInviteLinkScreen import OwnershipTransferController import OldChannelsController +import ComponentFlow +import AlertComponent +import AlertTransferHeaderComponent +import AvatarComponent private let rankMaxLength: Int32 = 16 @@ -23,6 +27,7 @@ private final class ChannelAdminControllerArguments { let context: AccountContext let updateAdminRights: (Bool) -> Void let toggleRight: (RightsItem, TelegramChatAdminRightsFlags, Bool) -> Void + let updateProcessJoinRequests: (Bool) -> Void let toggleRightWhileDisabled: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void let transferOwnership: () -> Void let updateRank: (String, String) -> Void @@ -33,10 +38,11 @@ private final class ChannelAdminControllerArguments { let toggleIsOptionExpanded: (RightsItem.Sub) -> Void let openPeer: () -> Void - init(context: AccountContext, updateAdminRights: @escaping (Bool) -> Void, toggleRight: @escaping (RightsItem, TelegramChatAdminRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void, toggleIsOptionExpanded: @escaping (RightsItem.Sub) -> Void, openPeer: @escaping () -> Void) { + init(context: AccountContext, updateAdminRights: @escaping (Bool) -> Void, toggleRight: @escaping (RightsItem, TelegramChatAdminRightsFlags, Bool) -> Void, updateProcessJoinRequests: @escaping (Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void, toggleIsOptionExpanded: @escaping (RightsItem.Sub) -> Void, openPeer: @escaping () -> Void) { self.context = context self.updateAdminRights = updateAdminRights self.toggleRight = toggleRight + self.updateProcessJoinRequests = updateProcessJoinRequests self.toggleRightWhileDisabled = toggleRightWhileDisabled self.transferOwnership = transferOwnership self.updateRank = updateRank @@ -80,6 +86,8 @@ private enum ChannelAdminEntryStableId: Hashable { case rightsTitle case right(RightsItem) case addAdminsInfo + case processJoinRequests + case processJoinRequestsInfo case transfer case dismiss } @@ -123,6 +131,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry { case rightsTitle(PresentationTheme, String) case rightItem(PresentationTheme, Int, String, RightsItem, TelegramChatAdminRightsFlags, Bool, Bool, [AdminSubPermission], Bool) case addAdminsInfo(PresentationTheme, String) + case processJoinRequests(PresentationTheme, String, Bool, Bool) + case processJoinRequestsInfo(PresentationTheme, String) case transfer(PresentationTheme, String) case dismiss(PresentationTheme, String) @@ -134,7 +144,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return ChannelAdminSection.rank.rawValue case .adminRights: return ChannelAdminSection.adminRights.rawValue - case .rightsTitle, .rightItem, .addAdminsInfo: + case .rightsTitle, .rightItem, .addAdminsInfo, .processJoinRequests, .processJoinRequestsInfo: return ChannelAdminSection.rights.rawValue case .transfer: return ChannelAdminSection.transfer.rawValue @@ -163,6 +173,10 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return .right(right) case .addAdminsInfo: return .addAdminsInfo + case .processJoinRequests: + return .processJoinRequests + case .processJoinRequestsInfo: + return .processJoinRequestsInfo case .transfer: return .transfer case .dismiss: @@ -269,6 +283,18 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } else { return false } + case let .processJoinRequests(lhsTheme, lhsText, lhsValue, lhsEnabled): + if case let .processJoinRequests(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .processJoinRequestsInfo(lhsTheme, lhsText): + if case let .processJoinRequestsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .transfer(lhsTheme, lhsText): if case let .transfer(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -323,37 +349,51 @@ private enum ChannelAdminEntry: ItemListNodeEntry { default: return true } + case .processJoinRequests: + switch rhs { + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .processJoinRequests: + return false + default: + return true + } + case .processJoinRequestsInfo: + switch rhs { + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .processJoinRequests, .processJoinRequestsInfo: + return false + default: + return true + } case .transfer: switch rhs { - case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .processJoinRequests, .processJoinRequestsInfo, .transfer: return false default: return true } case .rankTitle: switch rhs { - case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .processJoinRequests, .processJoinRequestsInfo, .transfer, .rankTitle: return false default: return true } case .rankPreview: switch rhs { - case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rankPreview: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .processJoinRequests, .processJoinRequestsInfo, .transfer, .rankTitle, .rankPreview: return false default: return true } case .rank: switch rhs { - case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rankPreview, .rank: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .processJoinRequests, .processJoinRequestsInfo, .transfer, .rankTitle, .rankPreview, .rank: return false default: return true } case .rankInfo: switch rhs { - case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .transfer, .rankTitle, .rankPreview, .rank, .rankInfo: + case .info, .adminRights, .rightsTitle, .rightItem, .addAdminsInfo, .processJoinRequests, .processJoinRequestsInfo, .transfer, .rankTitle, .rankPreview, .rank, .rankInfo: return false default: return true @@ -448,6 +488,13 @@ private enum ChannelAdminEntry: ItemListNodeEntry { } case let .addAdminsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .processJoinRequests(_, text, value, enabled): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, type: .icon, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateProcessJoinRequests(value) + }, activatedWhileDisabled: { + }) + case let .processJoinRequestsInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .transfer(_, text): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.transferOwnership() @@ -464,14 +511,16 @@ private struct ChannelAdminControllerState: Equatable { var adminRights: Bool var updatedFlags: TelegramChatAdminRightsFlags? var updatedRank: String? + var processJoinRequests: Bool? var updating: Bool var focusedOnRank: Bool var expandedPermissions: Set = Set() - init(adminRights: Bool = true, updatedFlags: TelegramChatAdminRightsFlags? = nil, updatedRank: String? = nil, updating: Bool = false, focusedOnRank: Bool = false, expandedPermissions: Set = Set()) { + init(adminRights: Bool = true, updatedFlags: TelegramChatAdminRightsFlags? = nil, updatedRank: String? = nil, processJoinRequests: Bool? = nil, updating: Bool = false, focusedOnRank: Bool = false, expandedPermissions: Set = Set()) { self.adminRights = adminRights self.updatedFlags = updatedFlags self.updatedRank = updatedRank + self.processJoinRequests = processJoinRequests self.updating = updating self.focusedOnRank = focusedOnRank self.expandedPermissions = expandedPermissions @@ -487,6 +536,9 @@ private struct ChannelAdminControllerState: Equatable { if lhs.updatedRank != rhs.updatedRank { return false } + if lhs.processJoinRequests != rhs.processJoinRequests { + return false + } if lhs.updating != rhs.updating { return false } @@ -500,23 +552,27 @@ private struct ChannelAdminControllerState: Equatable { } func withUpdatedAdminRights(_ adminRights: Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) + return ChannelAdminControllerState(adminRights: adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, processJoinRequests: self.processJoinRequests, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) } func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChatAdminRightsFlags?) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: updatedFlags, updatedRank: self.updatedRank, processJoinRequests: self.processJoinRequests, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) } - + func withUpdatedUpdatedRank(_ updatedRank: String?) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: updatedRank, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: updatedRank, processJoinRequests: self.processJoinRequests, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) + } + + func withUpdatedProcessJoinRequests(_ processJoinRequests: Bool) -> ChannelAdminControllerState { + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, processJoinRequests: processJoinRequests, updating: self.updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) } func withUpdatedUpdating(_ updating: Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, processJoinRequests: self.processJoinRequests, updating: updating, focusedOnRank: self.focusedOnRank, expandedPermissions: self.expandedPermissions) } func withUpdatedFocusedOnRank(_ focusedOnRank: Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, updating: self.updating, focusedOnRank: focusedOnRank, expandedPermissions: self.expandedPermissions) + return ChannelAdminControllerState(adminRights: self.adminRights, updatedFlags: self.updatedFlags, updatedRank: self.updatedRank, processJoinRequests: self.processJoinRequests, updating: self.updating, focusedOnRank: focusedOnRank, expandedPermissions: self.expandedPermissions) } } @@ -624,7 +680,68 @@ private func areAllAdminRightsEnabled(_ flags: TelegramChatAdminRightsFlags, pee return TelegramChatAdminRightsFlags.peerSpecific(peer: peer).subtracting(except).intersection(flags) == TelegramChatAdminRightsFlags.peerSpecific(peer: peer).subtracting(except) } -private func channelAdminControllerEntries(presentationData: PresentationData, state: ChannelAdminControllerState, accountPeerId: EnginePeer.Id, channelPeer: EnginePeer?, adminPeer: EnginePeer?, adminPresence: EnginePeer.Presence?, initialParticipant: ChannelParticipant?, invite: Bool, canEdit: Bool) -> [ChannelAdminEntry] { +private func guardBotAdminAlertText(_ text: String, presentationData: PresentationData) -> NSAttributedString { + return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: presentationData.theme.actionSheet.primaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: presentationData.theme.actionSheet.primaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: presentationData.theme.actionSheet.primaryTextColor), + linkAttribute: { _ in return nil } + ), textAlignment: .center) +} + +private func guardBotReplacementAlertController(context: AccountContext, presentationData: PresentationData, currentBot: EnginePeer, newBot: EnginePeer, commit: @escaping () -> Void) -> AlertScreen { + let currentBotName = currentBot.compactDisplayTitle + let newBotName = newBot.compactDisplayTitle + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertTransferHeaderComponent( + fromComponent: AnyComponentWithIdentity(id: "currentBot", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: currentBot + ) + )), + toComponent: AnyComponentWithIdentity(id: "newBot", component: AnyComponent( + AvatarComponent( + context: context, + theme: presentationData.theme, + peer: newBot + ) + )), + type: .transfer + ) + ) + )) + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: presentationData.strings.Channel_EditAdmin_GuardBotReplaceTitle) + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .attributed(guardBotAdminAlertText(presentationData.strings.Channel_EditAdmin_GuardBotReplaceText(currentBotName, newBotName).string, presentationData: presentationData))) + ) + )) + + return AlertScreen( + context: context, + configuration: AlertScreen.Configuration(actionAlignment: .vertical, dismissOnOutsideTap: true, allowInputInset: false), + content: content, + actions: [ + .init(title: presentationData.strings.Channel_EditAdmin_GuardBotReplaceKeep(currentBotName).string), + .init(title: presentationData.strings.Channel_EditAdmin_GuardBotReplaceUse(newBotName).string, type: .default, action: { + commit() + }) + ] + ) +} + +private func channelAdminControllerEntries(presentationData: PresentationData, state: ChannelAdminControllerState, accountPeerId: EnginePeer.Id, channelPeer: EnginePeer?, adminPeer: EnginePeer?, adminPresence: EnginePeer.Presence?, currentGuardBotId: EnginePeer.Id?, initialParticipant: ChannelParticipant?, invite: Bool, canEdit: Bool) -> [ChannelAdminEntry] { var entries: [ChannelAdminEntry] = [] if case let .channel(channel) = channelPeer, let admin = adminPeer { @@ -634,7 +751,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s if case .broadcast = channel.info { isChannel = true } - + var isCreator = false if let initialParticipant = initialParticipant, case .creator = initialParticipant { isCreator = true @@ -642,6 +759,12 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s var canTransfer = false var canDismiss = false + let canEditProcessJoinRequests: Bool + if case let .user(user) = admin, user.botInfo?.flags.contains(.isGuardBot) == true { + canEditProcessJoinRequests = canEdit && user.id != accountPeerId + } else { + canEditProcessJoinRequests = false + } let isGroup: Bool var maskRightsFlags: TelegramChatAdminRightsFlags @@ -697,7 +820,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s if isCreator { if isGroup { entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader)) - + let accountUserRightsFlags: TelegramChatAdminRightsFlags if channel.flags.contains(.isCreator) { accountUserRightsFlags = maskRightsFlags @@ -845,7 +968,11 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s index += 1 } - if accountUserRightsFlags.contains(.canAddAdmins) { + if canEditProcessJoinRequests { + let processJoinRequests = state.processJoinRequests ?? (currentGuardBotId == admin.id) + entries.append(.processJoinRequests(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionProcessJoinRequests, processJoinRequests, !state.updating)) + entries.append(.processJoinRequestsInfo(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionProcessJoinRequestsInfo)) + } else if accountUserRightsFlags.contains(.canAddAdmins) { entries.append(.addAdminsInfo(presentationData.theme, currentRightsFlags.contains(.canAddAdmins) ? presentationData.strings.Channel_EditAdmin_PermissinAddAdminOn : presentationData.strings.Channel_EditAdmin_PermissinAddAdminOff)) } @@ -912,7 +1039,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s } } } - + if !invite || state.adminRights { if canTransfer { entries.append(.transfer(presentationData.theme, isGroup ? presentationData.strings.Group_EditAdmin_TransferOwnership : presentationData.strings.Channel_EditAdmin_TransferOwnership)) @@ -1065,6 +1192,9 @@ public func channelAdminController(context: AccountContext, updatedPresentationD let transferOwnershipDisposable = MetaDisposable() actionsDisposable.add(transferOwnershipDisposable) + + let guardBotDisposable = MetaDisposable() + actionsDisposable.add(guardBotDisposable) var dismissImpl: (() -> Void)? var dismissInputImpl: (() -> Void)? @@ -1105,6 +1235,10 @@ public func channelAdminController(context: AccountContext, updatedPresentationD } return current.withUpdatedUpdatedFlags(updated) } + }, updateProcessJoinRequests: { value in + updateState { current in + return current.withUpdatedProcessJoinRequests(value) + } }, toggleRightWhileDisabled: { right, _ in let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), @@ -1248,16 +1382,140 @@ public func channelAdminController(context: AccountContext, updatedPresentationD ), context.engine.data.subscribe( TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: peerId) + ), + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.CachedData(id: peerId) ) ) |> deliverOnMainQueue - |> map { presentationData, state, peerInfoData, exportedInvitation -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, peerInfoData, exportedInvitation, cachedData -> (ItemListControllerState, (ItemListNodeState, Any)) in let channelPeer = peerInfoData.0.flatMap { $0 } let adminPeer = peerInfoData.1.flatMap { $0 } let adminPresence = peerInfoData.2 + let cachedChannelData = cachedData.flatMap { $0 as? CachedChannelData } + let currentGuardBotId = cachedChannelData?.guardBotId let canEdit = canEditAdminRights(accountPeerId: context.account.peerId, channelPeer: channelPeer!, initialParticipant: initialParticipant) + let channelIsGroup: Bool + let requestToJoinEnabled: Bool + if case let .channel(channel) = channelPeer { + if case .group = channel.info { + channelIsGroup = true + } else { + channelIsGroup = false + } + requestToJoinEnabled = channel.flags.contains(.requestToJoin) + } else { + channelIsGroup = true + requestToJoinEnabled = false + } + let canUseProcessJoinRequests: Bool + if case let .user(user) = adminPeer, user.botInfo?.flags.contains(.isGuardBot) == true, case .channel = channelPeer, canEdit, !invite || state.adminRights { + canUseProcessJoinRequests = true + } else { + canUseProcessJoinRequests = false + } + + let finishAfterSaving: (TelegramChatAdminRights?, Bool) -> Void = { adminRights, notifyUpdated in + let _ = (statePromise.get() + |> take(1)).start(next: { state in + let processJoinRequests = state.processJoinRequests ?? (currentGuardBotId == adminId) + + let complete: () -> Void = { + if notifyUpdated { + updated(adminRights) + } + dismissImpl?() + } + guard canUseProcessJoinRequests else { + complete() + return + } + + if processJoinRequests { + if currentGuardBotId == adminId { + complete() + return + } + if notifyUpdated { + updated(adminRights) + } + updateState { current in + return current.withUpdatedUpdating(true) + } + guardBotDisposable.set((context.engine.peers.toggleChannelJoinRequest(peerId: peerId, enabled: true, guardBotId: adminId, applyToInvites: false, clearGuardBot: false) + |> deliverOnMainQueue).start(error: { _ in + updateState { current in + return current.withUpdatedUpdating(false) + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + }, completed: { + dismissImpl?() + })) + } else if currentGuardBotId == adminId { + if notifyUpdated { + updated(adminRights) + } + updateState { current in + return current.withUpdatedUpdating(true) + } + guardBotDisposable.set((context.engine.peers.toggleChannelJoinRequest(peerId: peerId, enabled: requestToJoinEnabled, guardBotId: nil, applyToInvites: false, clearGuardBot: true) + |> deliverOnMainQueue).start(error: { _ in + updateState { current in + return current.withUpdatedUpdating(false) + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + }, completed: { + dismissImpl?() + })) + } else { + complete() + } + }) + } + + let confirmProcessJoinRequests: (@escaping () -> Void) -> Void = { commit in + let _ = (statePromise.get() + |> take(1)).start(next: { state in + let processJoinRequests = state.processJoinRequests ?? (currentGuardBotId == adminId) + + guard canUseProcessJoinRequests, processJoinRequests else { + commit() + return + } + if currentGuardBotId == nil { + guard let adminPeer else { + commit() + return + } + let title = channelIsGroup ? presentationData.strings.Group_Setup_ApproveNewMembers : presentationData.strings.Channel_Setup_ApproveNewSubscribers + let text = channelIsGroup ? presentationData.strings.Channel_EditAdmin_GuardBotEnableMembersText(adminPeer.compactDisplayTitle).string : presentationData.strings.Channel_EditAdmin_GuardBotEnableSubscribersText(adminPeer.compactDisplayTitle).string + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: title, text: text, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_EditAdmin_GuardBotEnable, action: { + commit() + }) + ], parseMarkdown: true), nil) + } else if currentGuardBotId != adminId, let currentGuardBotId, let adminPeer { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: currentGuardBotId)) + |> deliverOnMainQueue).start(next: { currentGuardPeer in + guard let currentGuardPeer else { + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + return + } + presentControllerImpl?(guardBotReplacementAlertController(context: context, presentationData: presentationData, currentBot: currentGuardPeer, newBot: adminPeer, commit: { + commit() + }), nil) + }) + } else { + commit() + } + }) + } let rightButtonActionImpl = { + confirmProcessJoinRequests { if invite && !state.adminRights { updateState { current in return current.withUpdatedUpdating(true) @@ -1411,8 +1669,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD } presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) }, completed: { - updated(TelegramChatAdminRights(rights: updateFlags)) - dismissImpl?() + finishAfterSaving(TelegramChatAdminRights(rights: updateFlags), true) })) } else if let updateRank = updateRank, let currentFlags = currentFlags { updateState { current in @@ -1421,11 +1678,10 @@ public func channelAdminController(context: AccountContext, updatedPresentationD updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(engine: context.engine, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: currentFlags), rank: updateRank) |> deliverOnMainQueue).start(error: { _ in }, completed: { - updated(TelegramChatAdminRights(rights: currentFlags)) - dismissImpl?() + finishAfterSaving(TelegramChatAdminRights(rights: currentFlags), true) })) } else { - dismissImpl?() + finishAfterSaving(nil, false) } } else if canEdit { var updateFlags: TelegramChatAdminRightsFlags? @@ -1500,8 +1756,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD } dismissImpl?() }, completed: { - updated(TelegramChatAdminRights(rights: updateFlags)) - dismissImpl?() + finishAfterSaving(TelegramChatAdminRights(rights: updateFlags), true) })) } } @@ -1631,6 +1886,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD dismissImpl?() } } + } } var footerButtonTitle: String = presentationData.strings.Channel_Management_SaveChanges @@ -1701,7 +1957,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD let rightNavigationButton: ItemListNavigationButton? if state.focusedOnRank { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { rightButtonActionImpl() }) footerItem = nil @@ -1711,7 +1967,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelPeer: channelPeer, adminPeer: adminPeer, adminPresence: adminPresence, initialParticipant: initialParticipant, invite: invite, canEdit: canEdit), style: .blocks, focusItemTag: nil, ensureVisibleItemTag: nil, emptyStateItem: nil, footerItem: footerItem, animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelPeer: channelPeer, adminPeer: adminPeer, adminPresence: adminPresence, currentGuardBotId: currentGuardBotId, initialParticipant: initialParticipant, invite: invite, canEdit: canEdit), style: .blocks, focusItemTag: nil, ensureVisibleItemTag: nil, emptyStateItem: nil, footerItem: footerItem, animateChanges: true) return (controllerState, (listState, arguments)) } diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift index 3ee97b784d..705c240543 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift @@ -973,7 +973,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation var secondaryRightNavigationButton: ItemListNavigationButton? if let admins = admins, admins.count > 1 { if state.editing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index f23d0903a7..96110577b5 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -13,6 +13,8 @@ import AlertUI import PresentationDataUtils import ItemListAvatarAndNameInfoItem import OldChannelsController +import ChatTimerScreen +import ContextUI private let rankMaxLength: Int32 = 16 @@ -54,6 +56,7 @@ private enum ChannelBannedMemberSection: Int32 { private enum ChannelBannedMemberEntryTag: ItemListItemTag { case rank + case timeout func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? ChannelBannedMemberEntryTag, self == other { @@ -361,7 +364,7 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { case let .timeout(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openTimeout() - }) + }, tag: ChannelBannedMemberEntryTag.timeout) case let .exceptionInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .delete(_, text): @@ -407,6 +410,18 @@ private struct ChannelBannedMemberControllerState: Equatable { var focusedOnRank: Bool } +private final class ChannelBannedMemberContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0)) + } +} + func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags { var result = flags result.remove(.banReadMessages) @@ -603,10 +618,12 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var dismissInputImpl: (() -> Void)? var errorImpl: (() -> Void)? var scrollToRankImpl: (() -> Void)? + var findTimeoutReferenceNode: (() -> ItemListDisclosureItemNode?)? let peerSignal = Promise() peerSignal.set(context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) @@ -714,12 +731,20 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } }, openTimeout: { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationData: presentationData) let intervals: [Int32] = [ 1 * 60 * 60 * 24, 7 * 60 * 60 * 24, 30 * 60 * 60 * 24 ] + let currentTimeout: Int32 = stateValue.with { state in + if let updatedTimeout = state.updatedTimeout { + return updatedTimeout + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _, _) = initialParticipant, let banInfo = maybeBanInfo { + return banInfo.rights.untilDate + } else { + return Int32.max + } + } let applyValue: (Int32?) -> Void = { value in updateState { state in var state = state @@ -727,29 +752,71 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen return state } } - var items: [ActionSheetItem] = [] + var items: [ContextMenuItem] = [] for interval in intervals { - items.append(ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: interval), color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - applyValue(initialState.referenceTimestamp + interval) - })) + let timeoutValue = initialState.referenceTimestamp + interval + items.append(.action(ContextMenuActionItem(text: timeIntervalString(strings: presentationData.strings, value: interval), icon: { theme in + if currentTimeout == timeoutValue { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) + applyValue(timeoutValue) + }))) } - items.append(ActionSheetButtonItem(title: presentationData.strings.MessageTimer_Forever, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MessageTimer_Forever, icon: { theme in + if currentTimeout == 0 || currentTimeout == Int32.max { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) applyValue(Int32.max) - })) - items.append(ActionSheetButtonItem(title: presentationData.strings.MessageTimer_Custom, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - presentControllerImpl?(PeerBanTimeoutController(context: context, updatedPresentationData: updatedPresentationData, currentValue: Int32(Date().timeIntervalSince1970), applyValue: { value in - applyValue(value) - }), nil) - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, nil) + }))) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MessageTimer_Custom, icon: { _ in + return nil + }, action: { _, f in + f(.default) + let controller = ChatTimerScreen( + context: context, + updatedPresentationData: updatedPresentationData, + configuration: ChatTimerScreen.Configuration( + style: .default, + picker: .date, + currentValue: Int32(Date().timeIntervalSince1970), + minimumDate: Date(), + maximumDate: Date(timeIntervalSince1970: Double(Int32.max - 1)), + pickerValueMapping: .roundDateToDaysUTC, + primaryActionTitle: { strings, _, _ in + strings.Wallpaper_Set + } + ), + completion: { value in + guard let value else { + return + } + applyValue(value) + } + ) + presentControllerImpl?(controller, nil) + }))) + guard let sourceNode = findTimeoutReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(ChannelBannedMemberContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) }, delete: { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) @@ -1055,7 +1122,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen let rightNavigationButton: ItemListNavigationButton? let footerItem: ItemListControllerFooterItem? if state.focusedOnRank { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { rightButtonActionImpl() }) footerItem = nil @@ -1084,9 +1151,15 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen presentControllerImpl = { [weak controller] value, presentationArguments in controller?.present(value, in: .window(.root), with: presentationArguments) } + presentInGlobalOverlayImpl = { [weak controller] value in + controller?.presentInGlobalOverlay(value, with: nil) + } pushControllerImpl = { [weak controller] c in controller?.push(c) } + findTimeoutReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: ChannelBannedMemberEntryTag.timeout) as? ItemListDisclosureItemNode + } let hapticFeedback = HapticFeedback() errorImpl = { [weak controller] in diff --git a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift index 563e800c12..b85436332f 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift @@ -454,7 +454,7 @@ public func channelBlacklistController(context: AccountContext, updatedPresentat var secondaryRightNavigationButton: ItemListNavigationButton? if let participants = participants, !participants.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 0019fb3423..a9311e1547 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -752,7 +752,7 @@ public func channelMembersController(context: AccountContext, updatedPresentatio } if !isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index c9bed2e235..236c5c511b 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -41,11 +41,12 @@ private final class ChannelVisibilityControllerArguments { let toggleForwarding: (Bool) -> Void let updateJoinToSend: (CurrentChannelJoinToSend) -> Void let toggleApproveMembers: (Bool) -> Void + let openGuardBot: (EnginePeer.Id) -> Void let activateLink: (String) -> Void let deactivateLink: (String) -> Void let openAuction: (String) -> Void - init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, revokePeerId: @escaping (EnginePeer.Id) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, linkContextAction: @escaping (ASDisplayNode, ContextGesture?) -> Void, manageInviteLinks: @escaping () -> Void, openLink: @escaping (ExportedInvitation) -> Void, toggleForwarding: @escaping (Bool) -> Void, updateJoinToSend: @escaping (CurrentChannelJoinToSend) -> Void, toggleApproveMembers: @escaping (Bool) -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void, openAuction: @escaping (String) -> Void) { + init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, scrollToPublicLinkText: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, revokePeerId: @escaping (EnginePeer.Id) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, linkContextAction: @escaping (ASDisplayNode, ContextGesture?) -> Void, manageInviteLinks: @escaping () -> Void, openLink: @escaping (ExportedInvitation) -> Void, toggleForwarding: @escaping (Bool) -> Void, updateJoinToSend: @escaping (CurrentChannelJoinToSend) -> Void, toggleApproveMembers: @escaping (Bool) -> Void, openGuardBot: @escaping (EnginePeer.Id) -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void, openAuction: @escaping (String) -> Void) { self.context = context self.updateCurrentType = updateCurrentType self.updatePublicLinkText = updatePublicLinkText @@ -60,6 +61,7 @@ private final class ChannelVisibilityControllerArguments { self.toggleForwarding = toggleForwarding self.updateJoinToSend = updateJoinToSend self.toggleApproveMembers = toggleApproveMembers + self.openGuardBot = openGuardBot self.activateLink = activateLink self.deactivateLink = deactivateLink self.openAuction = openAuction @@ -126,7 +128,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case joinToSendMembers(PresentationTheme, String, Bool) case approveMembers(PresentationTheme, String, Bool) - case approveMembersInfo(PresentationTheme, String) + case approveMembersInfo(PresentationTheme, String, EnginePeer.Id?) case forwardingHeader(PresentationTheme, String) case forwardingDisabled(PresentationTheme, String, Bool) @@ -386,8 +388,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { } else { return false } - case let .approveMembersInfo(lhsTheme, lhsText): - if case let .approveMembersInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .approveMembersInfo(lhsTheme, lhsText, lhsGuardBotId): + if case let .approveMembersInfo(rhsTheme, rhsText, rhsGuardBotId) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsGuardBotId == rhsGuardBotId { return true } else { return false @@ -740,8 +742,12 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: selected, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleApproveMembers(value) }) - case let .approveMembersInfo(_, text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .approveMembersInfo(_, text, guardBotId): + return ItemListTextItem(presentationData: presentationData, text: guardBotId == nil ? .plain(text) : .markdown(text), sectionId: self.section, linkAction: { _ in + if let guardBotId { + arguments.openGuardBot(guardBotId) + } + }) case let .forwardingHeader(_, title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .forwardingDisabled(_, text, selected): @@ -780,6 +786,7 @@ private struct ChannelVisibilityControllerState: Equatable { let forwardingEnabled: Bool? let joinToSend: CurrentChannelJoinToSend? let approveMembers: Bool? + let approveMembersApplyToInvites: Bool? init() { self.selectedType = nil @@ -792,9 +799,10 @@ private struct ChannelVisibilityControllerState: Equatable { self.forwardingEnabled = nil self.joinToSend = nil self.approveMembers = nil + self.approveMembersApplyToInvites = nil } - init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool, revealedRevokePeerId: EnginePeer.Id?, revokingPeerId: EnginePeer.Id?, revokingPrivateLink: Bool, forwardingEnabled: Bool?, joinToSend: CurrentChannelJoinToSend?, approveMembers: Bool?) { + init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool, revealedRevokePeerId: EnginePeer.Id?, revokingPeerId: EnginePeer.Id?, revokingPrivateLink: Bool, forwardingEnabled: Bool?, joinToSend: CurrentChannelJoinToSend?, approveMembers: Bool?, approveMembersApplyToInvites: Bool?) { self.selectedType = selectedType self.editingPublicLinkText = editingPublicLinkText self.addressNameValidationStatus = addressNameValidationStatus @@ -805,6 +813,7 @@ private struct ChannelVisibilityControllerState: Equatable { self.forwardingEnabled = forwardingEnabled self.joinToSend = joinToSend self.approveMembers = approveMembers + self.approveMembersApplyToInvites = approveMembersApplyToInvites } static func ==(lhs: ChannelVisibilityControllerState, rhs: ChannelVisibilityControllerState) -> Bool { @@ -838,47 +847,54 @@ private struct ChannelVisibilityControllerState: Equatable { if lhs.approveMembers != rhs.approveMembers { return false } + if lhs.approveMembersApplyToInvites != rhs.approveMembersApplyToInvites { + return false + } return true } func withUpdatedSelectedType(_ selectedType: CurrentChannelType?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) + return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: self.approveMembersApplyToInvites) } func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: self.approveMembersApplyToInvites) } func withUpdatedAddressNameValidationStatus(_ addressNameValidationStatus: AddressNameValidationStatus?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: self.approveMembersApplyToInvites) } func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: self.approveMembersApplyToInvites) } func withUpdatedRevealedRevokePeerId(_ revealedRevokePeerId: EnginePeer.Id?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: self.approveMembersApplyToInvites) } func withUpdatedRevokingPeerId(_ revokingPeerId: EnginePeer.Id?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: self.approveMembersApplyToInvites) } func withUpdatedRevokingPrivateLink(_ revokingPrivateLink: Bool) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: self.approveMembersApplyToInvites) } func withUpdatedForwardingEnabled(_ forwardingEnabled: Bool) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: self.approveMembersApplyToInvites) } func withUpdatedJoinToSend(_ joinToSend: CurrentChannelJoinToSend?) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: joinToSend, approveMembers: self.approveMembers) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: self.approveMembersApplyToInvites) } func withUpdatedApproveMembers(_ approveMembers: Bool) -> ChannelVisibilityControllerState { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: approveMembers) + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: approveMembers, approveMembersApplyToInvites: nil) + } + + func withUpdatedApproveMembersApplyToInvites(_ approveMembersApplyToInvites: Bool) -> ChannelVisibilityControllerState { + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId, revokingPrivateLink: self.revokingPrivateLink, forwardingEnabled: self.forwardingEnabled, joinToSend: self.joinToSend, approveMembers: self.approveMembers, approveMembersApplyToInvites: approveMembersApplyToInvites) } } @@ -1154,12 +1170,18 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa entries.append(.joinToSendEveryone(presentationData.theme, presentationData.strings.Group_Setup_WhoCanSendMessages_Everyone, joinToSend == .everyone)) entries.append(.joinToSendMembers(presentationData.theme, presentationData.strings.Group_Setup_WhoCanSendMessages_OnlyMembers, joinToSend == .members)) } - - if !isDiscussion || joinToSend == .members { - entries.append(.approveMembers(presentationData.theme, presentationData.strings.Group_Setup_ApproveNewMembers, approveMembers)) - entries.append(.approveMembersInfo(presentationData.theme, presentationData.strings.Group_Setup_ApproveNewMembersInfo)) - } } + + let approveMembersTitle = isGroup ? presentationData.strings.Group_Setup_ApproveNewMembers : presentationData.strings.Channel_Setup_ApproveNewSubscribers + var approveMembersInfo = isGroup ? presentationData.strings.Group_Setup_ApproveNewMembersInfo : presentationData.strings.Channel_Setup_ApproveNewSubscribersInfo + var guardBotId: EnginePeer.Id? + if let cachedChannelData = view.cachedData as? CachedChannelData, let currentGuardBotId = cachedChannelData.guardBotId, let guardBotPeer = view.peers[currentGuardBotId].flatMap(EnginePeer.init), let guardBotUsername = guardBotPeer.addressName, !guardBotUsername.isEmpty { + guardBotId = currentGuardBotId + approveMembersInfo += " " + presentationData.strings.Group_Setup_ApproveNewMembersManagedBy("[@\(guardBotUsername)](guardbot)").string + } + + entries.append(.approveMembers(presentationData.theme, approveMembersTitle, approveMembers)) + entries.append(.approveMembersInfo(presentationData.theme, approveMembersInfo, guardBotId)) entries.append(.forwardingHeader(presentationData.theme, isGroup ? presentationData.strings.Group_Setup_ForwardingGroupTitle.uppercased() : presentationData.strings.Group_Setup_ForwardingChannelTitle.uppercased())) entries.append(.forwardingDisabled(presentationData.theme, presentationData.strings.Group_Setup_ForwardingDisabled, !forwardingEnabled)) @@ -1458,6 +1480,13 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta actionsDisposable.add(toggleRequestToJoinDisposable) let temporaryOrder = Promise<[String]?>(nil) + let activeInviteLinksCount = Atomic(value: 0) + let currentPeerIsGroup = Atomic(value: true) + let activeInviteLinksContext = context.engine.peers.peerExportedInvitations(peerId: peerId, adminId: nil, revoked: false, forceUpdate: false) + actionsDisposable.add((activeInviteLinksContext.state + |> deliverOnMainQueue).start(next: { state in + let _ = activeInviteLinksCount.swap(state.hasLoadedOnce ? state.count : 0) + })) let arguments = ChannelVisibilityControllerArguments(context: context, updateCurrentType: { type in if type == .publicChannel { @@ -1697,9 +1726,34 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta return state.withUpdatedJoinToSend(value) } }, toggleApproveMembers: { value in - updateState { state in - return state.withUpdatedApproveMembers(value) + let updateApproveMembers: (Bool) -> Void = { applyToInvites in + updateState { state in + return state.withUpdatedApproveMembers(value).withUpdatedApproveMembersApplyToInvites(applyToInvites) + } } + + let inviteLinksCount = activeInviteLinksCount.with { $0 } + if inviteLinksCount > 0 { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let isGroup = currentPeerIsGroup.with { $0 } + let approvalTitle = isGroup ? presentationData.strings.Group_Setup_ApproveNewMembers : presentationData.strings.Channel_Setup_ApproveNewSubscribers + let action = value ? presentationData.strings.Group_Setup_ApproveNewMembersApplyToExistingInviteLinksEnable : presentationData.strings.Group_Setup_ApproveNewMembersApplyToExistingInviteLinksDisable + let peerType = isGroup ? presentationData.strings.Group_Setup_ApproveNewMembersApplyToExistingInviteLinksPeerGroup : presentationData.strings.Group_Setup_ApproveNewMembersApplyToExistingInviteLinksPeerChannel + let text = presentationData.strings.Group_Setup_ApproveNewMembersApplyToExistingInviteLinksText(action, approvalTitle, "\(inviteLinksCount)", peerType).string + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: presentationData.strings.Group_Setup_ApproveNewMembersApplyToExistingInviteLinksTitle, text: text, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: { + updateApproveMembers(false) + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { + updateApproveMembers(true) + }) + ], parseMarkdown: true), nil) + } else { + updateApproveMembers(false) + } + }, openGuardBot: { guardBotId in + let controller = channelAdminController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, adminId: guardBotId, initialParticipant: nil, updated: { _ in }, upgradedToSupergroup: upgradedToSupergroup, transferedOwnership: { _ in }) + pushControllerImpl?(controller) }, activateLink: { name in dismissInputImpl?() let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) @@ -1836,6 +1890,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta } else if let _ = peer as? TelegramGroup { isGroup = true } + let _ = currentPeerIsGroup.swap(isGroup) var rightNavigationButton: ItemListNavigationButton? if case .revokeNames = mode { @@ -1879,7 +1934,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta isInitialSetup = false } - rightNavigationButton = ItemListNavigationButton(content: .text(isInitialSetup ? presentationData.strings.Common_Next : presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: isInitialSetup ? .text(presentationData.strings.Common_Next) : .icon(.done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in updatedAddressNameValue = updatedAddressName(mode: mode, state: state, peer: .channel(peer), cachedData: view.cachedData) @@ -1899,7 +1954,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta } if let updatedApproveMembers = state.approveMembers { - toggleRequestToJoinDisposable.set(context.engine.peers.toggleChannelJoinRequest(peerId: peerId, enabled: updatedApproveMembers).start()) + toggleRequestToJoinDisposable.set(context.engine.peers.toggleChannelJoinRequest(peerId: peerId, enabled: updatedApproveMembers, guardBotId: nil, applyToInvites: state.approveMembersApplyToInvites ?? false).start()) } if let updatedAddressNameValue = updatedAddressNameValue { @@ -1975,7 +2030,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta } } - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in updatedAddressNameValue = updatedAddressName(mode: mode, state: state, peer: .legacyGroup(peer), cachedData: nil) @@ -2063,7 +2118,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta case .initialSetup: leftNavigationButton = nil case .generic, .privateLink, .revokeNames: - leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) } diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index aa571d51df..c7c27627a6 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -1089,7 +1089,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, case .vcard: break case .filter, .create: - leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?(true) cancelled?() }) @@ -1131,7 +1131,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, composedContactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: filteredPhoneNumbers), middleName: filteredData.middleName, prefix: filteredData.prefix, suffix: filteredData.suffix, organization: filteredData.organization, jobTitle: filteredData.jobTitle, department: filteredData.department, emailAddresses: filteredData.emailAddresses, urls: urls, addresses: filteredData.addresses, birthdayDate: filteredData.birthdayDate, socialProfiles: filteredData.socialProfiles, instantMessagingProfiles: filteredData.instantMessagingProfiles, note: filteredData.note) } - rightNavigationButton = ItemListNavigationButton(content: .text(isShare ? presentationData.strings.Common_Done : presentationData.strings.Compose_Create), style: .bold, enabled: (isShare || !filteredPhoneNumbers.isEmpty) && composedContactData != nil, action: { + rightNavigationButton = ItemListNavigationButton(content: isShare ? .icon(.done) : .text(presentationData.strings.Compose_Create), style: .bold, enabled: (isShare || !filteredPhoneNumbers.isEmpty) && composedContactData != nil, action: { if let composedContactData = composedContactData { guard let context = (context as? ShareControllerAppAccountContext)?.context else { return diff --git a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift index c9cecdc0bc..b7c3ca891f 100644 --- a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift +++ b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift @@ -144,14 +144,14 @@ public func groupPreHistorySetupController(context: AccountContext, updatedPrese |> deliverOnMainQueue |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, Any)) in let defaultValue: Bool = (view.cachedData as? CachedChannelData)?.flags.contains(.preHistoryEnabled) ?? false - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) var rightNavigationButton: ItemListNavigationButton? if state.applyingSetting { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { var value: Bool? updateState { state in var state = state diff --git a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift index 64133463f7..3683dcf37b 100644 --- a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift +++ b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift @@ -383,7 +383,7 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { imageNode.update(size: imageFitSize) } - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size) if let switchView = strongSelf.switchNode.view as? UISwitch { if strongSelf.switchNode.bounds.size.width.isZero { switchView.sizeToFit() diff --git a/submodules/PeerInfoUI/Sources/PeerAutoremoveSetupScreen.swift b/submodules/PeerInfoUI/Sources/PeerAutoremoveSetupScreen.swift index ec009e1670..6e4983cbcf 100644 --- a/submodules/PeerInfoUI/Sources/PeerAutoremoveSetupScreen.swift +++ b/submodules/PeerInfoUI/Sources/PeerAutoremoveSetupScreen.swift @@ -187,14 +187,14 @@ public func peerAutoremoveSetupScreen(context: AccountContext, updatedPresentati let peer = view.peers[view.peerId] - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) var rightNavigationButton: ItemListNavigationButton? if state.applyingSetting { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { var changedValue: Int32? updateState { state in var state = state diff --git a/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift b/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift deleted file mode 100644 index 396e10a4d6..0000000000 --- a/submodules/PeerInfoUI/Sources/PeerBanTimeoutController.swift +++ /dev/null @@ -1,127 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import UIKit -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import TelegramStringFormatting -import AccountContext -import UIKitRuntimeUtils - -final class PeerBanTimeoutController: ActionSheetController { - private var presentationDisposable: Disposable? - - private let _ready = Promise() - 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/PeerInfoUI/Sources/PhoneLabelController.swift b/submodules/PeerInfoUI/Sources/PhoneLabelController.swift index 5bb4a86ac6..ce2b21fb4a 100644 --- a/submodules/PeerInfoUI/Sources/PhoneLabelController.swift +++ b/submodules/PeerInfoUI/Sources/PhoneLabelController.swift @@ -110,11 +110,11 @@ public func phoneLabelController(context: AccountContext, currentLabel: String, let signal = combineLatest(context.sharedContext.presentationData, statePromise.get()) |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { arguments.cancel() }) - let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + let rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { arguments.complete() }) diff --git a/submodules/PeerInfoUI/Sources/SecretChatKeyControllerNode.swift b/submodules/PeerInfoUI/Sources/SecretChatKeyControllerNode.swift index 985a4bf850..7f75d032ee 100644 --- a/submodules/PeerInfoUI/Sources/SecretChatKeyControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/SecretChatKeyControllerNode.swift @@ -73,6 +73,8 @@ final class SecretChatKeyControllerNode: ViewControllerTracingNode { override func didLoad() { super.didLoad() + self.scrollNode.view.scrollsToTop = false + self.infoNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.infoTap(_:)))) } diff --git a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift index c3ff6f4c80..33b4bb07be 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoEditingPhoneItem.swift @@ -284,7 +284,7 @@ class UserInfoEditingPhoneItemNode: ItemListRevealOptionsItemNode, ItemListItemN strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)])) } }) } 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/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 9ffa3fb2b2..cd063c8afd 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -1438,7 +1438,7 @@ private final class PremiumBoostLevelsSheetComponent: CombinedComponent { ) ) }, - backgroundColor: .color(theme.list.modalBlocksBackgroundColor), + backgroundColor: .color(theme.list.modalPlainBackgroundColor), animateOut: animateOut ), environment: { @@ -1472,306 +1472,6 @@ private final class PremiumBoostLevelsSheetComponent: CombinedComponent { } } -//private final class BoostLevelsContainerComponent: CombinedComponent { -// class ExternalState { -// var isGroup: Bool = false -// var contentHeight: CGFloat = 0.0 -// } -// -// let context: AccountContext -// let theme: PresentationTheme -// let strings: PresentationStrings -// let externalState: ExternalState -// let peerId: EnginePeer.Id -// let mode: PremiumBoostLevelsScreen.Mode -// let status: ChannelBoostStatus? -// let boostState: InternalBoostState.DisplayData? -// let boost: () -> Void -// let copyLink: (String) -> Void -// let dismiss: () -> Void -// let openStats: (() -> Void)? -// let openGift: (() -> Void)? -// let openPeer: ((EnginePeer) -> Void)? -// let updated: () -> Void -// -// init( -// context: AccountContext, -// theme: PresentationTheme, -// strings: PresentationStrings, -// externalState: ExternalState, -// peerId: EnginePeer.Id, -// mode: PremiumBoostLevelsScreen.Mode, -// status: ChannelBoostStatus?, -// boostState: InternalBoostState.DisplayData?, -// boost: @escaping () -> Void, -// copyLink: @escaping (String) -> Void, -// dismiss: @escaping () -> Void, -// openStats: (() -> Void)?, -// openGift: (() -> Void)?, -// openPeer: ((EnginePeer) -> Void)?, -// updated: @escaping () -> Void -// ) { -// self.context = context -// self.theme = theme -// self.strings = strings -// self.externalState = externalState -// self.peerId = peerId -// self.mode = mode -// self.status = status -// self.boostState = boostState -// self.boost = boost -// self.copyLink = copyLink -// self.dismiss = dismiss -// self.openStats = openStats -// self.openGift = openGift -// self.openPeer = openPeer -// self.updated = updated -// } -// -// static func ==(lhs: BoostLevelsContainerComponent, rhs: BoostLevelsContainerComponent) -> Bool { -// if lhs.context !== rhs.context { -// return false -// } -// if lhs.theme !== rhs.theme { -// return false -// } -// if lhs.peerId != rhs.peerId { -// return false -// } -// if lhs.mode != rhs.mode { -// return false -// } -// if lhs.status != rhs.status { -// return false -// } -// if lhs.boostState != rhs.boostState { -// return false -// } -// return true -// } -// -// final class State: ComponentState { -// var topContentOffset: CGFloat = 0.0 -// var cachedStatsImage: (UIImage, PresentationTheme)? -// var cachedCloseImage: (UIImage, PresentationTheme)? -// -// private var disposable: Disposable? -// private(set) var peer: EnginePeer? -// -// init(context: AccountContext, peerId: EnginePeer.Id, updated: @escaping () -> Void) { -// super.init() -// -// self.disposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) -// |> deliverOnMainQueue).startStrict(next: { [weak self] peer in -// guard let self else { -// return -// } -// self.peer = peer -// updated() -// }) -// } -// -// deinit { -// self.disposable?.dispose() -// } -// } -// -// func makeState() -> State { -// return State(context: self.context, peerId: self.peerId, updated: self.updated) -// } -// -// static var body: Body { -// let background = Child(Rectangle.self) -// let scroll = Child(ScrollComponent.self) -// let topPanel = Child(BlurredBackgroundComponent.self) -// let topSeparator = Child(Rectangle.self) -// let title = Child(MultilineTextComponent.self) -// let statsButton = Child(Button.self) -// let closeButton = Child(Button.self) -// -// let externalScrollState = ScrollComponent.ExternalState() -// -// return { context in -// let state = context.state -// -// let theme = context.component.theme -// let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings -// -// let topInset: CGFloat = 56.0 -// -// let component = context.component -// -// var isGroup: Bool? -// if let peer = state.peer { -// if case let .channel(channel) = peer, case .group = channel.info { -// isGroup = true -// } else { -// isGroup = false -// } -// } -// -// if let isGroup { -// component.externalState.isGroup = isGroup -// let updated = component.updated -// let scroll = scroll.update( -// component: ScrollComponent( -// content: AnyComponent( -// SheetContent( -// context: component.context, -// theme: component.theme, -// strings: component.strings, -// insets: .zero, -// peerId: component.peerId, -// isGroup: isGroup, -// mode: component.mode, -// status: component.status, -// boostState: component.boostState, -// boost: component.boost, -// copyLink: component.copyLink, -// dismiss: component.dismiss, -// openStats: component.openStats, -// openGift: component.openGift, -// openPeer: component.openPeer, -// updated: { [weak state] in -// updated() -// } -// ) -// ), -// externalState: externalScrollState, -// contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), -// contentOffsetUpdated: { [weak state] topContentOffset, _ in -// state?.topContentOffset = topContentOffset -// Queue.mainQueue().justDispatch { -// state?.updated(transition: .immediate) -// } -// }, -// contentOffsetWillCommit: { _ in } -// ), -// availableSize: context.availableSize, -// transition: context.transition -// ) -// component.externalState.contentHeight = externalScrollState.contentHeight -// -// let background = background.update( -// component: Rectangle(color: theme.overallDarkAppearance ? theme.list.blocksBackgroundColor : theme.list.plainBackgroundColor), -// availableSize: scroll.size, -// transition: context.transition -// ) -// context.add(background -// .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) -// ) -// -// context.add(scroll -// .position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0)) -// ) -// } -// -// let titleString: String -// var titleFont = Font.semibold(17.0) -// -// switch component.mode { -// case let .owner(subject): -// if let status = component.status, let _ = status.nextLevelBoosts { -// if let subject { -// switch subject { -// case .stories: -// if status.level == 0 { -// titleString = strings.ChannelBoost_EnableStories -// } else { -// titleString = strings.ChannelBoost_IncreaseLimit -// } -// case .nameColors: -// titleString = strings.ChannelBoost_NameColor -// case .nameIcon: -// titleString = strings.ChannelBoost_NameIcon -// case .profileColors: -// titleString = strings.ChannelBoost_ProfileColor -// case .profileIcon: -// titleString = strings.ChannelBoost_ProfileIcon -// case .channelReactions: -// titleString = strings.ChannelBoost_CustomReactions -// case .emojiStatus: -// titleString = strings.ChannelBoost_EmojiStatus -// case .wallpaper: -// titleString = strings.ChannelBoost_Wallpaper -// case .customWallpaper: -// titleString = strings.ChannelBoost_CustomWallpaper -// case .audioTranscription: -// titleString = strings.GroupBoost_AudioTranscription -// case .emojiPack: -// titleString = strings.GroupBoost_EmojiPack -// case .noAds: -// titleString = strings.ChannelBoost_NoAds -// case .wearGift: -// titleString = strings.ChannelBoost_WearGift -// case .autoTranslate: -// titleString = strings.ChannelBoost_AutoTranslate -// } -// } else { -// titleString = isGroup == true ? strings.GroupBoost_Title_Current : strings.ChannelBoost_Title_Current -// } -// } else { -// titleString = strings.ChannelBoost_MaxLevelReached -// } -// case let .user(mode): -// var remaining: Int? -// if let status = component.status, let nextLevelBoosts = status.nextLevelBoosts { -// remaining = nextLevelBoosts - status.boosts -// } -// -// if let _ = remaining { -// if case .current = mode { -// titleString = isGroup == true ? strings.GroupBoost_Title_Current : strings.ChannelBoost_Title_Current -// } else { -// titleString = isGroup == true ? strings.GroupBoost_Title_Other : strings.ChannelBoost_Title_Other -// } -// } else { -// titleString = strings.ChannelBoost_MaxLevelReached -// } -// case .features: -// titleString = strings.GroupBoost_AdditionalFeatures -// titleFont = Font.semibold(20.0) -// } -// -// let title = title.update( -// component: MultilineTextComponent( -// text: .plain(NSAttributedString(string: titleString, font: titleFont, textColor: theme.rootController.navigationBar.primaryTextColor)), -// horizontalAlignment: .center, -// truncationType: .end, -// maximumNumberOfLines: 1 -// ), -// availableSize: context.availableSize, -// transition: context.transition -// ) -// -// let topPanelAlpha: CGFloat -// let titleOriginY: CGFloat -// let titleScale: CGFloat -// if case .features = component.mode { -// if state.topContentOffset > 78.0 { -// topPanelAlpha = min(30.0, state.topContentOffset - 78.0) / 30.0 -// } else { -// topPanelAlpha = 0.0 -// } -// -// let titleTopOriginY = topPanel.size.height / 2.0 -// let titleBottomOriginY: CGFloat = 146.0 -// let titleOriginDelta = titleTopOriginY - titleBottomOriginY -// -// let fraction = min(1.0, state.topContentOffset / abs(titleOriginDelta)) -// titleOriginY = titleBottomOriginY + fraction * titleOriginDelta -// titleScale = 1.0 - max(0.0, fraction * 0.2) -// } else { -// topPanelAlpha = min(30.0, state.topContentOffset) / 30.0 -// titleOriginY = topPanel.size.height / 2.0 -// titleScale = 1.0 -// } -// - -// } -// } -//} - public class PremiumBoostLevelsScreen: ViewControllerComponentContainer { public enum Mode: Equatable { public enum UserMode: Equatable { @@ -1844,1067 +1544,6 @@ public class PremiumBoostLevelsScreen: ViewControllerComponentContainer { } } -//public class PremiumBoostLevelsScreen: ViewController { -// public enum Mode: Equatable { -// public enum UserMode: Equatable { -// case external -// case current -// case groupPeer(EnginePeer.Id, Int) -// case unrestrict(Int) -// } -// case user(mode: UserMode) -// case owner(subject: BoostSubject?) -// case features -// } -// -// final class Node: ViewControllerTracingNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { -// private var presentationData: PresentationData -// private weak var controller: PremiumBoostLevelsScreen? -// -// let dim: ASDisplayNode -// let wrappingView: UIView -// let containerView: UIView -// -// let contentView: ComponentHostView -// let footerContainerView: UIView -// let footerView: ComponentHostView -// -// private let containerExternalState = BoostLevelsContainerComponent.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: PremiumBoostLevelsScreen) { -// 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.blocksBackgroundColor : self.presentationData.theme.list.plainBackgroundColor -// -// self.addSubnode(self.dim) -// -// self.view.addSubview(self.wrappingView) -// self.wrappingView.addSubview(self.containerView) -// self.containerView.addSubview(self.contentView) -// -// if case .user = controller.mode { -// self.containerView.addSubview(self.footerContainerView) -// self.footerContainerView.addSubview(self.footerView) -// } -// -// if let status = controller.status, let myBoostStatus = controller.myBoostStatus { -// var myBoostCount: Int32 = 0 -// var currentMyBoostCount: Int32 = 0 -// var availableBoosts: [MyBoostStatus.Boost] = [] -// var occupiedBoosts: [MyBoostStatus.Boost] = [] -// -// for boost in myBoostStatus.boosts { -// if let boostPeer = boost.peer { -// if boostPeer.id == controller.peerId { -// myBoostCount += 1 -// } else { -// occupiedBoosts.append(boost) -// } -// } else { -// availableBoosts.append(boost) -// } -// } -// -// let boosts = max(Int32(status.boosts), myBoostCount) -// let initialState = InternalBoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: boosts) -// self.boostState = initialState.displayData(myBoostCount: myBoostCount, currentMyBoostCount: 0, replacedBoosts: controller.replacedBoosts?.0) -// -// self.updatedState.set(.single(InternalBoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: boosts + 1))) -// -// if let (replacedBoosts, sourcePeers) = controller.replacedBoosts { -// currentMyBoostCount += 1 -// -// self.boostState = initialState.displayData(myBoostCount: myBoostCount, currentMyBoostCount: 1) -// Queue.mainQueue().justDispatch { -// self.updated(transition: .easeInOut(duration: 0.2)) -// } -// -// Queue.mainQueue().after(0.3) { -// let presentationData = context.sharedContext.currentPresentationData.with { $0 } -// -// var groupCount: Int32 = 0 -// var channelCount: Int32 = 0 -// for peer in sourcePeers { -// if case let .channel(channel) = peer { -// switch channel.info { -// case .broadcast: -// channelCount += 1 -// case .group: -// groupCount += 1 -// } -// } -// } -// let otherText: String -// if channelCount > 0 && groupCount == 0 { -// otherText = presentationData.strings.ReassignBoost_OtherChannels(channelCount) -// } else if groupCount > 0 && channelCount == 0 { -// otherText = presentationData.strings.ReassignBoost_OtherGroups(groupCount) -// } else { -// otherText = presentationData.strings.ReassignBoost_OtherGroupsAndChannels(Int32(sourcePeers.count)) -// } -// let text = presentationData.strings.ReassignBoost_Success(presentationData.strings.ReassignBoost_Boosts(replacedBoosts), otherText).string -// let undoController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "BoostReplace", scale: 0.066, colors: [:], title: nil, text: text, customUndoText: nil, timeout: 4.0), elevatedLayout: false, position: .top, action: { _ in return true }) -// controller.present(undoController, in: .current) -// } -// } -// -// self.availableBoosts = availableBoosts -// self.occupiedBoosts = occupiedBoosts -// self.myBoostCount = myBoostCount -// self.currentMyBoostCount = currentMyBoostCount -// } -// } -// -// 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) -// } -// -// private var boostState: InternalBoostState.DisplayData? -// func updated(transition: ComponentTransition, forceUpdate: Bool = false) { -// guard let controller = self.controller else { -// return -// } -// let contentSize = self.contentView.update( -// transition: transition, -// component: AnyComponent( -// BoostLevelsContainerComponent( -// context: controller.context, -// theme: self.presentationData.theme, -// strings: self.presentationData.strings, -// externalState: self.containerExternalState, -// peerId: controller.peerId, -// mode: controller.mode, -// status: controller.status, -// boostState: self.boostState, -// boost: { [weak controller] in -// guard let controller else { -// return -// } -// controller.node.updateBoostState() -// }, -// copyLink: { [weak self, weak controller] link in -// guard let self else { -// return -// } -// UIPasteboard.general.string = link -// -// if let previousController = controller?.navigationController?.viewControllers.reversed().first(where: { $0 !== controller }) as? ViewController { -// previousController.present(UndoOverlayController(presentationData: self.presentationData, content: .linkCopied(title: nil, text: self.presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current) -// } -// }, -// dismiss: { [weak controller] in -// controller?.dismiss(animated: true) -// }, -// openStats: controller.openStats, -// openGift: controller.openGift, -// openPeer: controller.openPeer, -// updated: { [weak self] in -// self?.requestLayout(transition: .immediate) -// } -// ) -// ), -// environment: {}, -// forceUpdate: forceUpdate, -// containerSize: self.containerView.bounds.size -// ) -// self.contentView.frame = CGRect(origin: .zero, size: contentSize) -// -// let footerHeight = self.footerHeight -// -// let actionTitle: String -// if self.currentMyBoostCount > 0 { -// actionTitle = self.presentationData.strings.ChannelBoost_BoostAgain -// } else { -// actionTitle = self.containerExternalState.isGroup ? self.presentationData.strings.GroupBoost_BoostGroup : self.presentationData.strings.ChannelBoost_BoostChannel -// } -// -// let footerSize = self.footerView.update( -// transition: .immediate, -// component: AnyComponent( -// FooterComponent( -// context: controller.context, -// theme: self.presentationData.theme, -// title: actionTitle, -// 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 { -// if let mode = self.controller?.mode, case .owner = mode { -// return 0.0 -// } -// -// 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 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 { -// 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 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 -// -// 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 -// -// let translation = recognizer.translation(in: self.view).y -// var velocity = recognizer.velocity(in: self.view) -// -// 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)) -// } -// -// private var currentMyBoostCount: Int32 = 0 -// private var myBoostCount: Int32 = 0 -// private var availableBoosts: [MyBoostStatus.Boost] = [] -// private var occupiedBoosts: [MyBoostStatus.Boost] = [] -// private let updatedState = Promise() -// -// private func updateBoostState() { -// guard let controller = self.controller else { -// return -// } -// let context = controller.context -// let peerId = controller.peerId -// let mode = controller.mode -// let status = controller.status -// let isPremium = controller.context.isPremium -// let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 })) -// let canBoostAgain = premiumConfiguration.boostsPerGiftCount > 0 -// let presentationData = self.presentationData -// let forceDark = controller.forceDark -// let boostStatusUpdated = controller.boostStatusUpdated -// -// if let _ = status?.nextLevelBoosts { -// if let availableBoost = self.availableBoosts.first { -// self.currentMyBoostCount += 1 -// self.myBoostCount += 1 -// -// let _ = (context.engine.peers.applyChannelBoost(peerId: peerId, slots: [availableBoost.slot]) -// |> deliverOnMainQueue).startStandalone(next: { [weak self] myBoostStatus in -// self?.updatedState.set(context.engine.peers.getChannelBoostStatus(peerId: peerId) -// |> beforeNext { [weak self] boostStatus in -// if let self, let boostStatus, let myBoostStatus { -// Queue.mainQueue().async { -// self.controller?.boostStatusUpdated(boostStatus, myBoostStatus) -// } -// } -// } -// |> map { status in -// if let status { -// return InternalBoostState(level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), boosts: Int32(status.boosts + 1)) -// } else { -// return nil -// } -// }) -// }) -// -// let _ = (self.updatedState.get() -// |> take(1) -// |> deliverOnMainQueue).startStandalone(next: { [weak self] state in -// guard let self, let state else { -// return -// } -// self.boostState = state.displayData(myBoostCount: self.myBoostCount, currentMyBoostCount: self.currentMyBoostCount) -// self.updated(transition: .easeInOut(duration: 0.2)) -// -// self.animateSuccess() -// }) -// -// self.availableBoosts.removeFirst() -// } else if !self.occupiedBoosts.isEmpty, let myBoostStatus = controller.myBoostStatus { -// if canBoostAgain { -// let navigationController = controller.navigationController -// let openPeer = controller.openPeer -// -// var dismissReplaceImpl: (() -> Void)? -// let replaceController = ReplaceBoostScreen(context: context, peerId: peerId, myBoostStatus: myBoostStatus, replaceBoosts: { slots in -// var sourcePeerIds = Set() -// var sourcePeers: [EnginePeer] = [] -// for boost in myBoostStatus.boosts { -// if slots.contains(boost.slot) { -// if let peer = boost.peer { -// if !sourcePeerIds.contains(peer.id) { -// sourcePeerIds.insert(peer.id) -// sourcePeers.append(peer) -// } -// } -// } -// } -// -// let _ = context.engine.peers.applyChannelBoost(peerId: peerId, slots: slots).startStandalone(completed: { -// let _ = combineLatest( -// queue: Queue.mainQueue(), -// context.engine.peers.getChannelBoostStatus(peerId: peerId), -// context.engine.peers.getMyBoostStatus() -// ).startStandalone(next: { boostStatus, myBoostStatus in -// dismissReplaceImpl?() -// -// if let boostStatus, let myBoostStatus { -// boostStatusUpdated(boostStatus, myBoostStatus) -// } -// -// let levelsController = PremiumBoostLevelsScreen( -// context: context, -// peerId: peerId, -// mode: mode, -// status: boostStatus, -// myBoostStatus: myBoostStatus, -// replacedBoosts: (Int32(slots.count), sourcePeers), -// openStats: nil, -// openGift: nil, -// openPeer: openPeer, -// forceDark: forceDark -// ) -// levelsController.boostStatusUpdated = boostStatusUpdated -// if let navigationController { -// navigationController.pushViewController(levelsController, animated: true) -// } -// }) -// }) -// }) -// -// if let navigationController = controller.navigationController { -// controller.dismiss(animated: true) -// navigationController.pushViewController(replaceController, animated: true) -// } -// -// dismissReplaceImpl = { [weak replaceController] in -// replaceController?.dismiss(animated: true) -// } -// } else if let boost = self.occupiedBoosts.first, let occupiedPeer = boost.peer { -// if let cooldown = boost.cooldownUntil { -// let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) -// let timeout = cooldown - currentTime -// let valueText = timeIntervalString(strings: presentationData.strings, value: timeout, usage: .afterTime, preferLowerValue: false) -// let alertController = textAlertController( -// sharedContext: context.sharedContext, -// updatedPresentationData: nil, -// title: presentationData.strings.ChannelBoost_Error_BoostTooOftenTitle, -// text: self.containerExternalState.isGroup ? presentationData.strings.GroupBoost_Error_BoostTooOftenText(valueText).string : presentationData.strings.ChannelBoost_Error_BoostTooOftenText(valueText).string, -// actions: [ -// TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) -// ], -// parseMarkdown: true -// ) -// controller.present(alertController, in: .window(.root)) -// } else { -// let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) -// |> deliverOnMainQueue).start(next: { [weak controller] peer in -// guard let peer, let controller else { -// return -// } -// let replaceController = replaceBoostConfirmationController(context: context, fromPeers: [occupiedPeer], toPeer: peer, commit: { [weak self] in -// self?.currentMyBoostCount += 1 -// self?.myBoostCount += 1 -// let _ = (context.engine.peers.applyChannelBoost(peerId: peerId, slots: [boost.slot]) -// |> deliverOnMainQueue).startStandalone(completed: { [weak self] in -// guard let self else { -// return -// } -// let _ = (self.updatedState.get() -// |> take(1) -// |> deliverOnMainQueue).startStandalone(next: { [weak self] state in -// guard let self, let state else { -// return -// } -// self.boostState = state.displayData(myBoostCount: self.myBoostCount, currentMyBoostCount: self.currentMyBoostCount) -// self.updated(transition: .easeInOut(duration: 0.2)) -// -// self.animateSuccess() -// }) -// }) -// }) -// controller.present(replaceController, in: .window(.root)) -// }) -// } -// } else { -// controller.dismiss(animated: true, completion: nil) -// } -// } else { -// if isPremium { -// if !canBoostAgain { -// controller.dismiss(animated: true, completion: nil) -// } else { -// let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) -// |> deliverOnMainQueue).start(next: { [weak controller] peer in -// guard let peer, let controller else { -// return -// } -// let alertController = textAlertController( -// sharedContext: context.sharedContext, -// updatedPresentationData: nil, -// title: presentationData.strings.ChannelBoost_MoreBoosts_Title, -// text: presentationData.strings.ChannelBoost_MoreBoosts_Text(peer.compactDisplayTitle, "\(premiumConfiguration.boostsPerGiftCount)").string, -// actions: [ -// TextAlertAction(type: .defaultAction, title: presentationData.strings.ChannelBoost_MoreBoosts_Gift, action: { [weak controller] in -// if let navigationController = controller?.navigationController { -// controller?.dismiss(animated: true, completion: nil) -// -// Queue.mainQueue().after(0.4) { -// let giftController = context.sharedContext.makePremiumGiftController(context: context, source: .channelBoost, completion: nil) -// navigationController.pushViewController(giftController, animated: true) -// } -// } -// }), -// TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {}) -// ], -// actionLayout: .vertical, -// parseMarkdown: true -// ) -// controller.present(alertController, in: .window(.root)) -// }) -// } -// } else { -// let alertController = textAlertController( -// sharedContext: context.sharedContext, -// updatedPresentationData: nil, -// title: presentationData.strings.ChannelBoost_Error_PremiumNeededTitle, -// text: self.containerExternalState.isGroup ? presentationData.strings.GroupBoost_Error_PremiumNeededText : presentationData.strings.ChannelBoost_Error_PremiumNeededText, -// actions: [ -// TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), -// TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { [weak controller] in -// if let navigationController = controller?.navigationController { -// controller?.dismiss(animated: true) -// -// let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .channelBoost(peerId), forceDark: forceDark, dismissed: nil) -// navigationController.pushViewController(premiumController, animated: true) -// } -// }) -// ], -// parseMarkdown: true -// ) -// controller.present(alertController, in: .window(.root)) -// } -// } -// } else { -// controller.dismiss(animated: true) -// } -// } -// -// func buttonPressed() { -// self.updateBoostState() -// } -// -// private func animateSuccess() { -// self.hapticFeedback.impact() -// self.view.addSubview(ConfettiView(frame: self.view.bounds)) -// -// if self.isExpanded { -// self.update(isExpanded: false, transition: .animated(duration: 0.4, curve: .spring)) -// } -// } -// } -// -// var node: Node { -// return self.displayNode as! Node -// } -// -// private let context: AccountContext -// private let peerId: EnginePeer.Id -// private let mode: Mode -// private let status: ChannelBoostStatus? -// private let myBoostStatus: MyBoostStatus? -// private let replacedBoosts: (Int32, [EnginePeer])? -// private let openStats: (() -> Void)? -// private let openGift: (() -> Void)? -// private let openPeer: ((EnginePeer) -> Void)? -// private let forceDark: Bool -// -// private var currentLayout: ContainerViewLayout? -// -// public var boostStatusUpdated: (ChannelBoostStatus, MyBoostStatus) -> Void = { _, _ in } -// public var disposed: () -> Void = {} -// -// public init( -// context: AccountContext, -// peerId: EnginePeer.Id, -// mode: Mode, -// status: ChannelBoostStatus?, -// myBoostStatus: MyBoostStatus? = nil, -// replacedBoosts: (Int32, [EnginePeer])? = nil, -// openStats: (() -> Void)? = nil, -// openGift: (() -> Void)? = nil, -// openPeer: ((EnginePeer) -> Void)? = nil, -// forceDark: Bool = false -// ) { -// self.context = context -// self.peerId = peerId -// self.mode = mode -// self.status = status -// self.myBoostStatus = myBoostStatus -// self.replacedBoosts = replacedBoosts -// self.openStats = openStats -// self.openGift = openGift -// self.openPeer = openPeer -// self.forceDark = forceDark -// -// super.init(navigationBarPresentationData: nil) -// -// self.navigationPresentation = .flatModal -// self.statusBar.statusBarStyle = .Ignore -// -// self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) -// } -// -// required public init(coder aDecoder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -// -// deinit { -// self.disposed() -// } -// -//} - -//private final class FooterComponent: Component { -// let context: AccountContext -// let theme: PresentationTheme -// let title: String -// let action: () -> Void -// -// init(context: AccountContext, theme: PresentationTheme, title: String, action: @escaping () -> Void) { -// self.context = context -// self.theme = theme -// self.title = title -// 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 -// } -// return true -// } -// -// 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) -// } -// -// 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))) -// -// let gradientColors = [ -// UIColor(rgb: 0x0077ff), -// UIColor(rgb: 0x6b93ff), -// UIColor(rgb: 0x8878ff), -// UIColor(rgb: 0xe46ace) -// ] -// -// let buttonSize = self.button.update( -// transition: .immediate, -// component: AnyComponent( -// SolidRoundedButtonComponent( -// title: component.title, -// theme: SolidRoundedButtonComponent.Theme( -// backgroundColor: .black, -// backgroundColors: gradientColors, -// foregroundColor: .white -// ), -// font: .bold, -// fontSize: 17.0, -// height: 50.0, -// cornerRadius: 10.0, -// gloss: true, -// iconName: "Premium/BoostChannel", -// animationName: nil, -// iconPosition: .left, -// action: { -// component.action() -// } -// ) -// ), -// 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 -// } -// -// return availableSize -// } -// } -// -// 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 struct InternalBoostState: Equatable { let level: Int32 let currentLevelBoosts: Int32 diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 0217b4dd9c..e9fe7c4fd2 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -12,11 +12,12 @@ import ViewControllerComponent import SheetComponent import MultilineTextComponent import BundleIconComponent -import SolidRoundedButtonComponent +import ButtonComponent import BlurredBackgroundComponent import Markdown import TelegramUIPreferences import GlassBarButtonComponent +import LottieComponent public final class PremiumGradientBackgroundComponent: Component { public let colors: [UIColor] @@ -671,7 +672,7 @@ private final class DemoSheetContent: CombinedComponent { let closeButton = Child(GlassBarButtonComponent.self) let background = Child(PremiumGradientBackgroundComponent.self) let pager = Child(DemoPagerComponent.self) - let button = Child(SolidRoundedButtonComponent.self) + let button = Child(ButtonComponent.self) let measureText = Child(MultilineTextComponent.self) return { context in @@ -1326,28 +1327,46 @@ private final class DemoSheetContent: CombinedComponent { } let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let premiumGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] + var buttonTitle: [AnyComponentWithIdentity] = [] + buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ButtonTextContentComponent( + text: buttonText, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: premiumGradientColors[0] + )))) + if isStandalone, let buttonAnimationName { + buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: buttonAnimationName), + color: .white, + startingPosition: .begin, + size: CGSize(width: 30.0, height: 30.0), + loop: true + )))) + } let button = button.update( - component: SolidRoundedButtonComponent( - title: buttonText, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: .black, - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: premiumGradientColors[0], + foreground: .white, + pressedColor: premiumGradientColors[0], + isShimmering: state.isPremium != true, + gradient: ButtonComponent.Background.Gradient( + colors: premiumGradientColors, + animation: .horizontalShift(duration: 4.5) + ) + ), + content: AnyComponentWithIdentity( + id: AnyHashable("\(buttonText)-\(isStandalone ? buttonAnimationName ?? "" : "")"), + component: AnyComponent(HStack(buttonTitle, spacing: 4.0)) ), - font: .bold, - fontSize: 17.0, - height: 52.0, - cornerRadius: 26.0, - gloss: state.isPremium != true, - glass: true, - animationName: isStandalone ? buttonAnimationName : nil, - iconPosition: .right, - iconSpacing: 4.0, action: { [weak component, weak state] in guard let component = component else { return diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index c2d9c19c5b..7d95e05186 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -8,10 +8,9 @@ import TelegramPresentationData import PresentationDataUtils import ViewControllerComponent import AccountContext -import SolidRoundedButtonComponent import MultilineTextComponent import BundleIconComponent -import SolidRoundedButtonComponent +import ButtonComponent import BlurredBackgroundComponent import Markdown import InAppPurchaseManager @@ -1015,7 +1014,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { let secondaryTitle = Child(MultilineTextComponent.self) let bottomPanel = Child(BlurredBackgroundComponent.self) let bottomSeparator = Child(Rectangle.self) - let button = Child(SolidRoundedButtonComponent.self) + let button = Child(ButtonComponent.self) return { context in let environment = context.environment[EnvironmentType.self].value @@ -1233,28 +1232,38 @@ private final class PremiumGiftScreenComponent: CombinedComponent { buttonText = environment.strings.Premium_Gift_GiftSubscription(price ?? "—").string } + let buttonGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] let button = button.update( - component: SolidRoundedButtonComponent( - title: buttonText, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: UIColor(rgb: 0x8878ff), - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: buttonGradientColors[0], + foreground: .white, + pressedColor: buttonGradientColors[0], + isShimmering: gloss, + gradient: ButtonComponent.Background.Gradient(colors: buttonGradientColors) ), - height: 50.0, - cornerRadius: 11.0, - gloss: gloss, - isLoading: state.inProgress, + content: AnyComponentWithIdentity( + id: AnyHashable(buttonText), + component: AnyComponent(ButtonTextContentComponent( + text: buttonText, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: buttonGradientColors[0] + )) + ), + displaysProgress: state.inProgress, action: { state.buy() } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 52.0), transition: context.transition) let bottomPanel = bottomPanel.update( diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index c7c0c12e3e..e825015ca7 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -10,7 +10,6 @@ import TelegramPresentationData import PresentationDataUtils import ViewControllerComponent import AccountContext -import SolidRoundedButtonComponent import ButtonComponent import MultilineTextComponent import MultilineTextWithEntitiesComponent @@ -3416,7 +3415,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let title = Child(MultilineTextComponent.self) let secondaryTitle = Child(MultilineTextWithEntitiesComponent.self) let bottomEdgeEffect = Child(EdgeEffectComponent.self) - let button = Child(SolidRoundedButtonComponent.self) + let button = Child(ButtonComponent.self) var updatedInstalled: Bool? @@ -3645,8 +3644,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 @@ -3817,25 +3824,51 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } let controller = environment.controller + let buttonGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] + let buttonContent: AnyComponent + if let buttonSubtitle { + buttonContent = AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(Text( + text: buttonTitle, + font: Font.semibold(17.0), + color: .white + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(Text( + text: buttonSubtitle, + font: Font.medium(11.0), + color: UIColor.white.withAlphaComponent(0.7) + ))) + ], spacing: 1.0)) + } else { + buttonContent = AnyComponent(ButtonTextContentComponent( + text: buttonTitle, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: buttonGradientColors[0] + )) + } let button = button.update( - component: SolidRoundedButtonComponent( - title: buttonTitle, - subtitle: buttonSubtitle, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: UIColor(rgb: 0x8878ff), - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: UIColor(rgb: 0x8878ff), + foreground: .white, + pressedColor: UIColor(rgb: 0x8878ff).withMultipliedAlpha(0.8), + cornerRadius: 26.0, + isShimmering: true, + gradient: ButtonComponent.Background.Gradient(colors: buttonGradientColors) ), - height: 52.0, - cornerRadius: 26.0, - gloss: true, - glass: true, - isLoading: state.inProgress, + content: AnyComponentWithIdentity( + id: AnyHashable("\(buttonTitle)-\(buttonSubtitle ?? "")"), + component: buttonContent + ), + displaysProgress: state.inProgress, action: { if let controller = controller() as? PremiumIntroScreen, let customProceed = controller.customProceed { controller.dismiss() diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index b487f1ea42..d60ecf31bd 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -13,6 +13,8 @@ import SheetComponent import MultilineTextComponent import BundleIconComponent import SolidRoundedButtonComponent +import ButtonComponent +import LottieComponent import Markdown import BalancedTextComponent import ConfettiEffect @@ -832,7 +834,7 @@ private final class LimitSheetContent: CombinedComponent { let alternateText = Child(List.self) let limit = Child(PremiumLimitDisplayComponent.self) let linkButton = Child(SolidRoundedButtonComponent.self) - let button = Child(SolidRoundedButtonComponent.self) + let button = Child(ButtonComponent.self) let peerShortcut = Child(Button.self) let statsButton = Child(Button.self) @@ -1456,23 +1458,44 @@ private final class LimitSheetContent: CombinedComponent { let isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let buttonTitle = actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK) + var buttonContentItems: [AnyComponentWithIdentity] = [] + if let buttonIconName { + buttonContentItems.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent( + name: buttonIconName, + tintColor: .white + )))) + } + buttonContentItems.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(ButtonTextContentComponent( + text: buttonTitle, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: buttonGradientColors[0] + )))) + if buttonIconName == nil, isIncreaseButton, let buttonAnimationName { + buttonContentItems.append(AnyComponentWithIdentity(id: "animation", component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: buttonAnimationName), + color: .white, + startingPosition: .begin, + size: CGSize(width: 30.0, height: 30.0), + loop: true + )))) + } let button = button.update( - component: SolidRoundedButtonComponent( - title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK), - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: .black, - backgroundColors: buttonGradientColors, - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: buttonGradientColors[0], + foreground: .white, + pressedColor: buttonGradientColors[0], + isShimmering: isIncreaseButton && actionButtonHasGloss, + gradient: ButtonComponent.Background.Gradient(colors: buttonGradientColors) + ), + content: AnyComponentWithIdentity( + id: AnyHashable("\(buttonTitle)-\(buttonIconName ?? "")-\(isIncreaseButton ? buttonAnimationName ?? "" : "")"), + component: AnyComponent(HStack(buttonContentItems, spacing: 4.0)) ), - font: .bold, - fontSize: 17.0, - height: 52.0, - cornerRadius: 26.0, - gloss: isIncreaseButton && actionButtonHasGloss, - glass: true, - iconName: buttonIconName, - animationName: isIncreaseButton ? buttonAnimationName : nil, - iconPosition: buttonIconName != nil ? .left : .right, action: { if isIncreaseButton { if component.action() { diff --git a/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift b/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift index 7fc6496107..af9c412f2b 100644 --- a/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift @@ -13,6 +13,7 @@ import BundleIconComponent import BalancedTextComponent import MultilineTextComponent import SolidRoundedButtonComponent +import ButtonComponent import LottieComponent import AccountContext import GlassBarButtonComponent @@ -110,7 +111,7 @@ private final class SheetContent: CombinedComponent { let premiumTitle = Child(BalancedTextComponent.self) let premiumText = Child(BalancedTextComponent.self) - let premiumButton = Child(SolidRoundedButtonComponent.self) + let premiumButton = Child(ButtonComponent.self) return { context in let environment = context.environment[EnvironmentType.self] @@ -342,28 +343,32 @@ private final class SheetContent: CombinedComponent { contentSize.height += premiumText.size.height contentSize.height += spacing + 5.0 + let premiumGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] let premiumButton = premiumButton.update( - component: SolidRoundedButtonComponent( - title: premiumButtonTitle, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: .black, - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: premiumGradientColors[0], + foreground: .white, + pressedColor: premiumGradientColors[0], + isShimmering: false, + gradient: ButtonComponent.Background.Gradient(colors: premiumGradientColors) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(premiumButtonTitle), + component: AnyComponent(ButtonTextContentComponent( + text: premiumButtonTitle, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: premiumGradientColors[0] + )) ), - font: .bold, - fontSize: 17.0, - height: 52.0, - cornerRadius: 26.0, - gloss: false, - glass: true, - iconName: nil, - animationName: nil, - iconPosition: .left, action: { component.openPremiumIntro() component.dismiss() diff --git a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift index 5a6b093777..bd014a9bdb 100644 --- a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift @@ -335,6 +335,7 @@ private class StickersCarouselNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.canCancelContentTouches = true + self.scrollNode.view.scrollsToTop = false self.tapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.stickerTapped(_:)))) } diff --git a/submodules/PresentationDataUtils/BUILD b/submodules/PresentationDataUtils/BUILD index 2b33e0a691..69e2a085c5 100644 --- a/submodules/PresentationDataUtils/BUILD +++ b/submodules/PresentationDataUtils/BUILD @@ -19,9 +19,7 @@ swift_library( "//submodules/ItemListUI:ItemListUI", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/OverlayStatusController:OverlayStatusController", - "//submodules/UrlWhitelist:UrlWhitelist", "//submodules/TelegramUI/Components/AlertComponent", - "//submodules/UrlHandling", ], visibility = [ "//visibility:public", diff --git a/submodules/QrCodeUI/BUILD b/submodules/QrCodeUI/BUILD index e7eb7e221b..6bb6fe0349 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", @@ -22,7 +23,6 @@ swift_library( "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/PresentationDataUtils:PresentationDataUtils", - "//submodules/GlassButtonNode:GlassButtonNode", "//submodules/TextFormat:TextFormat", "//submodules/Markdown:Markdown", "//submodules/UndoUI:UndoUI", @@ -34,12 +34,15 @@ swift_library( "//submodules/ComponentFlow", "//submodules/Components/SheetComponent", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/Components/BundleIconComponent", "//submodules/Components/BalancedTextComponent", "//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/GlassButtonNode/Sources/GlassButtonNode.swift b/submodules/QrCodeUI/Sources/GlassButtonNode.swift similarity index 72% rename from submodules/GlassButtonNode/Sources/GlassButtonNode.swift rename to submodules/QrCodeUI/Sources/GlassButtonNode.swift index d1f39743df..9bdb0d776d 100644 --- a/submodules/GlassButtonNode/Sources/GlassButtonNode.swift +++ b/submodules/QrCodeUI/Sources/GlassButtonNode.swift @@ -3,6 +3,8 @@ import Display import UIKit import AsyncDisplayKit import SwiftSignalKit +import ComponentFlow +import GlassBackgroundComponent private let largeButtonSize = CGSize(width: 72.0, height: 72.0) private let smallButtonSize = CGSize(width: 60.0, height: 60.0) @@ -29,15 +31,15 @@ private func generateEmptyButtonImage(icon: UIImage?, strokeColor: UIColor?, fil } let imageSize = icon.size let imageRect = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.width - imageSize.height) / 2.0)), size: imageSize) + + context.setBlendMode(.copy) + context.clip(to: imageRect, mask: icon.cgImage!) if knockout { - context.setBlendMode(.copy) - context.clip(to: imageRect, mask: icon.cgImage!) context.setFillColor(UIColor.clear.cgColor) - context.fill(imageRect) } else { - context.setBlendMode(.normal) - context.draw(icon.cgImage!, in: imageRect) + context.setFillColor(UIColor.white.cgColor) } + context.fill(imageRect) } }) } @@ -60,45 +62,44 @@ private func generateFilledButtonImage(color: UIColor, icon: UIImage?, angle: CG }) } -private let emptyHighlightedFill = UIColor(white: 1.0, alpha: 0.3) -private let invertedFill = UIColor(white: 1.0, alpha: 1.0) - private let largeLabelFont = Font.regular(14.5) private let smallLabelFont = Font.regular(11.5) -public final class GlassButtonNode: HighlightTrackingButtonNode { +final class GlassButtonNode: ASDisplayNode { private var regularImage: UIImage? - private var highlightedImage: UIImage? private var filledImage: UIImage? - private let blurView: UIVisualEffectView + private let backgroundView: GlassBackgroundView private let iconNode: ASImageNode private var labelNode: ImmediateTextNode? - public init(icon: UIImage, label: String?) { - let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - blurView.clipsToBounds = true - blurView.isUserInteractionEnabled = false - self.blurView = blurView + private let button: HighlightTrackingButton + + var pressed: () -> Void = {} + + init(icon: UIImage, label: String?) { + self.backgroundView = GlassBackgroundView() + + self.button = HighlightTrackingButton() self.iconNode = ASImageNode() - self.iconNode.isLayerBacked = true self.iconNode.displayWithoutProcessing = false self.iconNode.displaysAsynchronously = false + self.iconNode.isUserInteractionEnabled = false self.regularImage = generateEmptyButtonImage(icon: icon, strokeColor: nil, fillColor: .clear, buttonSize: largeButtonSize) - self.highlightedImage = generateEmptyButtonImage(icon: icon, strokeColor: nil, fillColor: emptyHighlightedFill, buttonSize: largeButtonSize) - self.filledImage = generateEmptyButtonImage(icon: icon, strokeColor: nil, fillColor: invertedFill, knockout: true, buttonSize: largeButtonSize) + self.filledImage = generateEmptyButtonImage(icon: icon, strokeColor: nil, fillColor: .white, knockout: true, buttonSize: largeButtonSize) if let label = label { let labelNode = ImmediateTextNode() let labelFont: UIFont - if let image = regularImage, image.size.width < 70.0 { + if let image = self.regularImage, image.size.width < 70.0 { labelFont = smallLabelFont } else { labelFont = largeLabelFont } labelNode.attributedText = NSAttributedString(string: label, font: labelFont, textColor: .white) + labelNode.isUserInteractionEnabled = false self.labelNode = labelNode } else { self.labelNode = nil @@ -106,38 +107,34 @@ public final class GlassButtonNode: HighlightTrackingButtonNode { super.init() - self.view.addSubview(blurView) - self.addSubnode(self.iconNode) + self.view.addSubview(self.backgroundView) + self.backgroundView.contentView.addSubview(self.button) + self.backgroundView.contentView.addSubview(self.iconNode.view) if let labelNode = self.labelNode { - self.addSubnode(labelNode) + self.backgroundView.contentView.addSubview(labelNode.view) } - self.iconNode.image = regularImage - self.currentImage = regularImage + self.iconNode.image = self.regularImage + self.currentImage = self.regularImage - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - strongSelf.internalHighlighted = highlighted - strongSelf.updateState(highlighted: highlighted, selected: strongSelf.isSelected) - } - } + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) } - private var internalHighlighted = false + @objc private func buttonPressed() { + self.pressed() + } - override public var isSelected: Bool { + var isSelected: Bool = false { didSet { - self.updateState(highlighted: self.internalHighlighted, selected: self.isSelected) + self.updateState(selected: self.isSelected) } } private var currentImage: UIImage? - private func updateState(highlighted: Bool, selected: Bool) { + private func updateState(selected: Bool) { let image: UIImage? if selected { image = self.filledImage - } else if highlighted { - image = self.highlightedImage } else { image = self.regularImage } @@ -160,8 +157,10 @@ public final class GlassButtonNode: HighlightTrackingButtonNode { let size = self.bounds.size - self.blurView.layer.cornerRadius = size.width / 2.0 - blurView.frame = self.bounds + self.button.frame = self.bounds + + self.backgroundView.frame = self.bounds + self.backgroundView.update(size: size, cornerRadius: size.width / 2.0, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate) self.iconNode.frame = self.bounds diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index 3a54062f87..bee53f957d 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift @@ -3,9 +3,9 @@ import UIKit import AccountContext import AsyncDisplayKit import Display +import ComponentFlow import SwiftSignalKit import Camera -import GlassButtonNode import CoreImage import AlertUI import TelegramPresentationData @@ -18,6 +18,8 @@ import LegacyComponents import LegacyMediaPickerUI import ImageContentAnalysis import PresentationDataUtils +import BundleIconComponent +import GlassBarButtonComponent private func parseAuthTransferUrl(_ url: URL) -> Data? { var tokenString: String? @@ -77,18 +79,14 @@ public final class QrCodeScanScreen: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - let navigationBarTheme = NavigationBarTheme(overallDarkAppearance: self.presentationData.theme.overallDarkAppearance, buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: .white, accentDisabledButtonColor: .white, accentForegroundColor: .black) - - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) + super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .White self.navigationPresentation = .modalInLargeLayout self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.navigationBar?.intrinsicCanTransitionInline = false - - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - + self.inForegroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground |> deliverOnMainQueue).start(next: { [weak self] inForeground in guard let strongSelf = self else { @@ -96,14 +94,6 @@ public final class QrCodeScanScreen: ViewController { } (strongSelf.displayNode as! QrCodeScanScreenNode).updateInForeground(inForeground) }) - - if case .custom = subject { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) - } else { - #if DEBUG - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Test", style: .plain, target: self, action: #selector(self.testPressed)) - #endif - } } required init(coder aDecoder: NSCoder) { @@ -116,19 +106,11 @@ public final class QrCodeScanScreen: ViewController { self.approveDisposable.dispose() } - @objc private func cancelPressed() { + @objc fileprivate func cancelPressed() { self.completion(nil) self.dismissAnimated() } - @objc private func myCodePressed() { - self.showMyCode() - } - - @objc private func testPressed() { - self.dismissWithSession(session: nil) - } - private var animatedIn = false public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -370,6 +352,7 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, ASScrollVie private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode private let errorTextNode: ImmediateTextNode + private let topNavigationButton = ComponentView() private let camera: Camera private let codeDisposable = MetaDisposable() @@ -510,10 +493,15 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, ASScrollVie self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.errorTextNode) - - self.galleryButtonNode.addTarget(self, action: #selector(self.galleryPressed), forControlEvents: .touchUpInside) - self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside) - + + self.galleryButtonNode.pressed = { [weak self] in + self?.galleryPressed() + } + + self.torchButtonNode.pressed = { [weak self] in + self?.torchPressed() + } + self.previewView.resetPlaceholder(front: false) if #available(iOS 13.0, *) { let _ = (self.previewView.isPreviewing @@ -666,6 +654,56 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, ASScrollVie transition.updateFrame(view: self.previewView, frame: bounds) transition.updateFrame(node: self.fadeNode, frame: bounds) + let topNavigationIconName: String + if case .custom = self.subject { + topNavigationIconName = "Navigation/Close" + } else { + topNavigationIconName = "Navigation/Back" + } + let topNavigationButtonSide = CGSize(width: 44.0, height: 44.0) + let topNavigationButtonSize = self.topNavigationButton.update( + transition: ComponentTransition(transition), + component: AnyComponent(GlassBarButtonComponent( + size: topNavigationButtonSide, + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity(id: topNavigationIconName, component: AnyComponent( + BundleIconComponent( + name: topNavigationIconName, + tintColor: .white + ) + )), + action: { [weak self] _ in + guard let self else { + return + } + if case .custom = self.subject { + self.controller?.cancelPressed() + } else { + self.controller?.dismiss() + } + } + )), + environment: {}, + containerSize: topNavigationButtonSide + ) + if let topNavigationButtonView = self.topNavigationButton.view { + if topNavigationButtonView.superview == nil { + self.view.addSubview(topNavigationButtonView) + } + transition.updateFrame( + view: topNavigationButtonView, + frame: CGRect( + origin: CGPoint( + x: 16.0 + layout.safeInsets.left, + y: max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + 5.0 + ), + size: topNavigationButtonSize + ) + ) + } + let frameSide = max(240.0, layout.size.width - sideInset * 2.0) let animateInScale: CGFloat = 0.4 var effectiveFrameSide = frameSide @@ -897,4 +935,3 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, ASScrollVie return true } } - 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/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 89d3a1cee3..bcb2e7d26d 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -533,6 +533,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.canCancelContentTouches = true + self.scrollNode.view.scrollsToTop = false self.scrollNode.clipsToBounds = false if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index b0ecc4368e..b8a0540a12 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -54,6 +54,7 @@ swift_library( "//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI", "//submodules/PhoneInputNode:PhoneInputNode", "//submodules/PhotoResources:PhotoResources", + "//submodules/StickerResources:StickerResources", "//submodules/ProgressNavigationButtonNode:ProgressNavigationButtonNode", "//submodules/RadialStatusNode:RadialStatusNode", "//submodules/SearchBarNode:SearchBarNode", @@ -67,7 +68,6 @@ swift_library( "//submodules/TextFormat:TextFormat", "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/UrlEscaping:UrlEscaping", - "//submodules/WebSearchUI:WebSearchUI", "//submodules/UrlHandling:UrlHandling", "//submodules/HexColor:HexColor", "//submodules/QrCode:QrCode", @@ -128,6 +128,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PasskeysScreen", "//submodules/TelegramUI/Components/FaceScanScreen", "//submodules/ComponentFlow", + "//submodules/Components/ResizableSheetComponent", "//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/ButtonComponent", @@ -137,6 +138,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/AvatarEditorScreen", "//submodules/TelegramUI/Components/Settings/PeerSelectionScreen", + "//submodules/TelegramUI/Components/Settings/ChatbotSetupScreen", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/Utils/DeviceModel", diff --git a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift index 8b2ab482a5..e02fd7938c 100644 --- a/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift +++ b/submodules/SettingsUI/Sources/BubbleSettings/BubbleSettingsController.swift @@ -133,6 +133,7 @@ private final class BubbleSettingsControllerNode: ASDisplayNode, ASScrollViewDel self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.alwaysBounceHorizontal = false + self.scrollNode.view.scrollsToTop = false } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index bb8871dfd9..699e3c24de 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -9,7 +9,6 @@ import TelegramUIPreferences import ItemListUI import PresentationDataUtils import AccountContext -import OpenInExternalAppUI import ItemListPeerActionItem import StorageUsageScreen import PresentationDataUtils @@ -33,7 +32,6 @@ private final class DataAndStorageControllerArguments { let togglePauseMusicOnRecording: (Bool) -> Void let toggleRaiseToListen: (Bool) -> Void let toggleDownloadInBackground: (Bool) -> Void - let openBrowserSelection: () -> Void let openIntents: () -> Void let toggleSensitiveContent: (Bool) -> Void @@ -49,7 +47,6 @@ private final class DataAndStorageControllerArguments { togglePauseMusicOnRecording: @escaping (Bool) -> Void, toggleRaiseToListen: @escaping (Bool) -> Void, toggleDownloadInBackground: @escaping (Bool) -> Void, - openBrowserSelection: @escaping () -> Void, openIntents: @escaping () -> Void, toggleSensitiveContent: @escaping (Bool) -> Void ) { @@ -64,7 +61,6 @@ private final class DataAndStorageControllerArguments { self.togglePauseMusicOnRecording = togglePauseMusicOnRecording self.toggleRaiseToListen = toggleRaiseToListen self.toggleDownloadInBackground = toggleDownloadInBackground - self.openBrowserSelection = openBrowserSelection self.openIntents = openIntents self.toggleSensitiveContent = toggleSensitiveContent } @@ -118,7 +114,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry { case useLessVoiceData(PresentationTheme, String, Bool) case useLessVoiceDataInfo(PresentationTheme, String) case otherHeader(PresentationTheme, String) - case openLinksIn(PresentationTheme, String, String) case shareSheet(PresentationTheme, String) case saveEditedPhotos(PresentationTheme, String, Bool) case pauseMusicOnRecording(PresentationTheme, String, Bool) @@ -143,7 +138,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return DataAndStorageSection.backgroundDownload.rawValue case .useLessVoiceData, .useLessVoiceDataInfo: return DataAndStorageSection.voiceCalls.rawValue - case .otherHeader, .openLinksIn, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo: + case .otherHeader, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo: return DataAndStorageSection.other.rawValue case .sensitiveContent, .sensitiveContentInfo: return DataAndStorageSection.sensitiveContent.rawValue @@ -182,8 +177,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return 24 case .otherHeader: return 29 - case .openLinksIn: - return 30 case .shareSheet: return 31 case .saveEditedPhotos: @@ -279,12 +272,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry { } else { return false } - case let .openLinksIn(lhsTheme, lhsText, lhsValue): - if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } case let .shareSheet(lhsTheme, lhsText): if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -414,10 +401,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .otherHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .openLinksIn(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, action: { - arguments.openBrowserSelection() - }) case let .shareSheet(_, text): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openIntents() @@ -618,7 +601,7 @@ private func autosaveLabelAndValue(presentationData: PresentationData, settings: return (label, value) } -private func dataAndStorageControllerEntries(context: AccountContext, state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, defaultWebBrowser: String, contentSettingsConfiguration: ContentSettingsConfiguration?, networkUsage: Int64, storageUsage: Int64, mediaAutoSaveSettings: MediaAutoSaveSettings, autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?], mediaSettings: MediaDisplaySettings, showSensitiveContentSetting: Bool) -> [DataAndStorageEntry] { +private func dataAndStorageControllerEntries(context: AccountContext, state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, contentSettingsConfiguration: ContentSettingsConfiguration?, networkUsage: Int64, storageUsage: Int64, mediaAutoSaveSettings: MediaAutoSaveSettings, autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?], mediaSettings: MediaDisplaySettings, showSensitiveContentSetting: Bool) -> [DataAndStorageEntry] { var entries: [DataAndStorageEntry] = [] entries.append(.storageUsage(presentationData.theme, presentationData.strings.ChatSettings_Cache, dataSizeString(storageUsage, formatting: DataSizeStringFormatting(presentationData: presentationData)))) @@ -648,7 +631,6 @@ private func dataAndStorageControllerEntries(context: AccountContext, state: Dat entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription)) entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other)) - entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser)) if #available(iOSApplicationExtension 13.2, iOS 13.2, *) { entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings)) } @@ -896,9 +878,6 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da settings.downloadInBackground = value return settings }).start() - }, openBrowserSelection: { - let controller = webBrowserSettingsController(context: context) - pushControllerImpl?(controller) }, openIntents: { let controller = intentsSettingsController(context: context) pushControllerImpl?(controller) @@ -952,26 +931,15 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da context.sharedContext.presentationData, statePromise.get(), dataAndStorageDataPromise.get(), - context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings, ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.mediaDisplaySettings]), contentSettingsConfiguration.get(), preferences, usageSignal, autosaveExceptionPeers ) |> map { presentationData, state, dataAndStorageData, sharedData, contentSettingsConfiguration, mediaAutoSaveSettings, usageSignal, autosaveExceptionPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in - let webBrowserSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings let mediaSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.mediaDisplaySettings]?.get(MediaDisplaySettings.self) ?? MediaDisplaySettings.defaultSettings - let options = availableOpenInOptions(context: context, item: .url(url: "https://telegram.org")) - let defaultWebBrowser: String - if let option = options.first(where: { $0.identifier == webBrowserSettings.defaultWebBrowser }) { - defaultWebBrowser = option.title - } else if webBrowserSettings.defaultWebBrowser == "inApp" { - defaultWebBrowser = presentationData.strings.WebBrowser_InAppSafari - } else { - defaultWebBrowser = presentationData.strings.WebBrowser_Telegram - } - let previousSensitiveContent = sensitiveContent.swap(contentSettingsConfiguration?.sensitiveContentEnabled) var animateChanges = false if previousSensitiveContent != contentSettingsConfiguration?.sensitiveContentEnabled { @@ -984,7 +952,7 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da let showSensitiveContentSetting = canAdjustSensitiveContent.with { $0 } ?? false let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataAndStorageControllerEntries(context: context, state: state, data: dataAndStorageData, presentationData: presentationData, defaultWebBrowser: defaultWebBrowser, contentSettingsConfiguration: contentSettingsConfiguration, networkUsage: usageSignal.network, storageUsage: usageSignal.storage, mediaAutoSaveSettings: mediaAutoSaveSettings, autosaveExceptionPeers: autosaveExceptionPeers, mediaSettings: mediaSettings, showSensitiveContentSetting: showSensitiveContentSetting), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataAndStorageControllerEntries(context: context, state: state, data: dataAndStorageData, presentationData: presentationData, contentSettingsConfiguration: contentSettingsConfiguration, networkUsage: usageSignal.network, storageUsage: usageSignal.storage, mediaAutoSaveSettings: mediaAutoSaveSettings, autosaveExceptionPeers: autosaveExceptionPeers, mediaSettings: mediaSettings, showSensitiveContentSetting: showSensitiveContentSetting), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index 18bbed17c7..1d6821467c 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift @@ -428,9 +428,13 @@ public func proxySettingsController(accountManager: AccountManager map { presentationData, state, proxySettings, statuses, connectionStatus -> (ItemListControllerState, (ItemListNodeState, Any)) in + var presentationData = presentationData + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + var leftNavigationButton: ItemListNavigationButton? if case .modal = mode { - leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) } @@ -439,7 +443,7 @@ public func proxySettingsController(accountManager: AccountManager() - 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..985e7d3fde 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)? @@ -332,10 +319,14 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: let signal = combineLatest(updatedPresentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: { + var presentationData = presentationData + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) - let rightNavigationButton = ItemListNavigationButton(content: .text("___done"), style: .bold, enabled: state.isComplete, action: { + let rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: state.isComplete, action: { if let proxyServerSettings = proxyServerSettings(with: state) { let _ = (updateProxySettingsInteractively(accountManager: accountManager, { settings in var settings = settings @@ -370,8 +361,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 +372,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/ProxySettingsServerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift index 3108d64985..a9dd996e54 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxySettingsServerItem.swift @@ -18,7 +18,7 @@ struct ProxySettingsServerItemEditing: Equatable { let revealed: Bool } -final class ProxySettingsServerItem: ListViewItem, ItemListItem { +final class ProxySettingsServerItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem { let theme: PresentationTheme let strings: PresentationStrings let systemStyle: ItemListSystemStyle @@ -34,6 +34,10 @@ final class ProxySettingsServerItem: ListViewItem, ItemListItem { let infoAction: () -> Void let setServerWithRevealedOptions: (ProxyServerSettings?, ProxyServerSettings?) -> Void let removeServer: (ProxyServerSettings) -> Void + + var hasActiveRevealOptions: Bool { + return self.editing.revealed + } init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, server: ProxyServerSettings, activity: Bool, active: Bool, color: ItemListCheckboxItemColor, label: String, labelAccent: Bool, editing: ProxySettingsServerItemEditing, sectionId: ItemListSectionId, action: @escaping () -> Void, infoAction: @escaping () -> Void, setServerWithRevealedOptions: @escaping (ProxyServerSettings?, ProxyServerSettings?) -> Void, removeServer: @escaping (ProxyServerSettings) -> Void) { self.theme = theme @@ -123,6 +127,7 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { private var item: ProxySettingsServerItem? private var layoutParams: ListViewItemLayoutParams? + private var isHighlighted = false override var canBeSelected: Bool { if self.editableControlNode != nil { @@ -232,7 +237,7 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { let peerRevealOptions: [ItemListRevealOption] if item.editing.editable { - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.theme.list.itemSecondaryTextColor)] } else { peerRevealOptions = [] } @@ -383,26 +388,29 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false + let topStripeIsHidden: Bool switch neighbors.top { case .sameSection(false): - strongSelf.topStripeNode.isHidden = true + topStripeIsHidden = true default: hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners + topStripeIsHidden = hasCorners } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat + let bottomStripeIsHidden: Bool switch neighbors.bottom { case .sameSection(false): bottomStripeInset = leftInset + editingOffset bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false + bottomStripeIsHidden = false default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners + bottomStripeIsHidden = hasCorners } + strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil @@ -432,7 +440,7 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { strongSelf.infoButtonNode.isUserInteractionEnabled = revealOffset.isZero && !item.editing.editing strongSelf.infoButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 55.0, y: 0.0), size: CGSize(width: 55.0, height: layout.contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) + strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)), transition: transition) strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) @@ -446,39 +454,8 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } + self.isHighlighted = highlighted + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.4, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) } override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { @@ -522,10 +499,16 @@ private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { var infoIconFrame = self.infoIconNode.frame infoIconFrame.origin.x = offset + params.width - params.rightInset - 55.0 + floor((55.0 - infoIconFrame.width) / 2.0) transition.updateFrame(node: self.infoIconNode, frame: infoIconFrame) - + self.infoButtonNode.isUserInteractionEnabled = offset.isZero } - + + override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) + } + override func revealOptionsInteractivelyOpened() { if let item = self.item { item.setServerWithRevealedOptions(item.server, nil) diff --git a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift index 4ab5be12f8..6a7d098788 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift @@ -660,7 +660,7 @@ public func saveIncomingMediaController(context: AccountContext, scope: SaveInco switch scope { case .peer, .addPeer: - rightButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { switch scope { case let .addPeer(_, completion): let configuration = stateValue.with({ $0 }).pendingConfiguration 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/Data and Storage/StorageUsageExceptionsScreen.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift index 643a4b2550..1460732cce 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift @@ -149,7 +149,7 @@ private enum StorageUsageExceptionsEntry: ItemListNodeEntry { switch self { case let .addException(text): let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme) - return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: text, alwaysPlain: false, sectionId: self.section, editing: false, action: { + return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: icon, title: text, alwaysPlain: false, sectionId: self.section, editing: false, action: { arguments.openAddException() }) case let .exceptionsHeader(text): @@ -173,7 +173,7 @@ private enum StorageUsageExceptionsEntry: ItemListNodeEntry { title = peer.peer.displayTitle(strings: presentationData.strings, displayOrder: .firstLast) } - return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: peer.peer, title: title, enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, context: arguments.context, iconPeer: peer.peer, title: title, enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { arguments.openPeerMenu(peer.peer.id, value) }, tag: StorageUsageExceptionsEntryTag.peer(peer.peer.id)) } @@ -289,15 +289,9 @@ public func storageUsageExceptionsScreen( } } - var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)? - let _ = presentControllerImpl var pushControllerImpl: ((ViewController) -> Void)? - var findPeerReferenceNode: ((EnginePeer.Id) -> ItemListDisclosureItemNode?)? - let _ = findPeerReferenceNode - var presentInGlobalOverlay: ((ViewController) -> Void)? - let _ = presentInGlobalOverlay let actionDisposables = DisposableSet() @@ -440,7 +434,7 @@ public func storageUsageExceptionsScreen( ) |> deliverOnMainQueue |> map { presentationData, peerExceptions, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = isModal ? ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) : nil @@ -458,9 +452,6 @@ public func storageUsageExceptionsScreen( controller.navigationPresentation = .modal controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) } - presentControllerImpl = { [weak controller] c, contextType, a in - controller?.present(c, in: contextType, with: a) - } pushControllerImpl = { [weak controller] c in controller?.push(c) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift index 29197201c1..14d3d23236 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift @@ -7,7 +7,7 @@ import TelegramPresentationData import TelegramCore import AccountContext import ItemListUI -import PhotoResources +import StickerResources private enum RevealOptionKey: Int32 { case delete @@ -19,7 +19,7 @@ final class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem { let context: AccountContext let title: String let label: String - let icon: TelegramMediaImage? + let favicon: Int64? let sectionId: ItemListSectionId let style: ItemListStyle let deleted: (() -> Void)? @@ -30,7 +30,7 @@ final class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem { context: AccountContext, title: String, label: String, - icon: TelegramMediaImage?, + favicon: Int64?, sectionId: ItemListSectionId, style: ItemListStyle, deleted: (() -> Void)? @@ -40,7 +40,7 @@ final class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem { self.context = context self.title = title self.label = label - self.icon = icon + self.favicon = favicon self.sectionId = sectionId self.style = style self.deleted = deleted @@ -96,9 +96,11 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It let labelNode: TextNode private let activateArea: AccessibilityAreaNode + private let iconDisposable = MetaDisposable() private var item: WebBrowserDomainExceptionItem? private var layoutParams: ListViewItemLayoutParams? + private var currentIconFile: TelegramMediaFile? override public var canBeSelected: Bool { return false @@ -140,6 +142,10 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It self.addSubnode(self.activateArea) } + + deinit { + self.iconDisposable.dispose() + } func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) @@ -216,12 +222,33 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It strongSelf.backgroundNode.backgroundColor = itemBackgroundColor } - let iconSize = CGSize(width: 40.0, height: 40.0) - var imageSize = iconSize - if currentItem?.icon?.id != item.icon?.id, let icon = item.icon { - strongSelf.iconNode.setSignal(chatMessagePhoto(mediaBox: item.context.sharedContext.accountManager.mediaBox, userLocation: .other, photoReference: .standalone(media: icon))) + let iconSize = CGSize(width: 30.0, height: 30.0) + if currentItem?.favicon != item.favicon { + strongSelf.currentIconFile = nil + strongSelf.iconDisposable.set(nil) + + if let favicon = item.favicon { + strongSelf.iconDisposable.set((item.context.engine.stickers.resolveInlineStickers(fileIds: [favicon]) + |> deliverOnMainQueue).start(next: { [weak strongSelf] files in + guard let strongSelf, strongSelf.item?.favicon == favicon, let file = files[favicon] else { + return + } + strongSelf.currentIconFile = file + var resolvedImageSize = iconSize + if let dimensions = file.dimensions?.cgSize { + resolvedImageSize = dimensions.aspectFilled(resolvedImageSize) + } + if file.isAnimatedSticker || file.isVideoSticker { + strongSelf.iconNode.setSignal(chatMessageAnimatedSticker(postbox: item.context.account.postbox, userLocation: .other, file: file, small: false, size: resolvedImageSize, fetched: true)) + } else { + strongSelf.iconNode.setSignal(chatMessageSticker(account: item.context.account, userLocation: .other, file: file, small: false, fetched: true)) + } + strongSelf.iconNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 8.0), imageSize: resolvedImageSize, boundingSize: iconSize, intrinsicInsets: .zero))() + })) + } } - if let icon = item.icon, let dimensions = largestImageRepresentation(icon.representations)?.dimensions.cgSize { + var imageSize = iconSize + if strongSelf.currentIconFile?.fileId.id == item.favicon, let dimensions = strongSelf.currentIconFile?.dimensions?.cgSize { imageSize = dimensions.aspectFilled(imageSize) } @@ -296,7 +323,7 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It let labelFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame - let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 11.0 + strongSelf.revealOffset, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0)), size: iconSize) + let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 16.0 + strongSelf.revealOffset, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0)), size: iconSize) strongSelf.iconNode.frame = iconFrame strongSelf.iconNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 7.0), imageSize: imageSize, boundingSize: iconSize, intrinsicInsets: .zero))() @@ -304,7 +331,7 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) var revealOptions: [ItemListRevealOption] = [] - revealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)) + revealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)) strongSelf.setRevealOptions((left: [], right: revealOptions)) } }) @@ -330,7 +357,7 @@ final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, It let leftInset: CGFloat = 16.0 + params.leftInset + 46.0 var iconFrame = self.iconNode.frame - iconFrame.origin.x = params.leftInset + 11.0 + offset + iconFrame.origin.x = params.leftInset + 16.0 + offset transition.updateFrame(node: self.iconNode, frame: iconFrame) var titleFrame = self.titleNode.frame diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift index ecaa103547..44f4a7c967 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserItem.swift @@ -11,22 +11,24 @@ import OpenInExternalAppUI import AccountContext import AppBundle -class WebBrowserItem: ListViewItem, ItemListItem { +final class WebBrowserItem: ListViewItem, ItemListItem { let context: AccountContext let presentationData: ItemListPresentationData let systemStyle: ItemListSystemStyle let title: String let application: OpenInApplication? + let identifier: String? let checked: Bool public let sectionId: ItemListSectionId let action: () -> Void - public init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, title: String, application: OpenInApplication?, checked: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + public init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, title: String, application: OpenInApplication?, identifier: String?, checked: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { self.context = context self.presentationData = presentationData self.systemStyle = systemStyle self.title = title self.application = application + self.identifier = identifier self.checked = checked self.sectionId = sectionId self.action = action @@ -148,11 +150,16 @@ private final class WebBrowserItemNode: ListViewItemNode { if currentItem == nil { switch item.application { case .none: - let icons = item.context.sharedContext.applicationBindings.getAvailableAlternateIcons() - let current = item.context.sharedContext.applicationBindings.getAlternateIconName() - let currentIcon = icons.first(where: { $0.name == current })?.imageName ?? "BlueIcon" - if let image = UIImage(named: currentIcon, in: getAppBundle(), compatibleWith: nil) { - updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image)) + if item.identifier == "default" { + let image = renderSettingsIcon(name: "Chat/Context Menu/Globe", backgroundColors: [UIColor(rgb: 0x8E8E93)])! + updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image), withChrome: false) + } else { + let icons = item.context.sharedContext.applicationBindings.getAvailableAlternateIcons() + let current = item.context.sharedContext.applicationBindings.getAlternateIconName() + let currentIcon = icons.first(where: { $0.name == current })?.imageName ?? "BlueIcon" + if let image = UIImage(named: currentIcon, in: getAppBundle(), compatibleWith: nil) { + updatedIconSignal = openInAppIcon(engine: item.context.engine, appIcon: .image(image: image)) + } } case .safari: if let image = UIImage(bundleImageName: "Open In/Safari") { diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index 018f3d97b8..7eafa1200a 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -12,10 +12,7 @@ import OpenInExternalAppUI import ItemListPeerActionItem import UndoUI import WebKit -import LinkPresentation -import CoreServices import PersistentStringHash -import UrlHandling private final class WebBrowserSettingsControllerArguments { let context: AccountContext @@ -23,7 +20,7 @@ private final class WebBrowserSettingsControllerArguments { let clearCookies: () -> Void let clearCache: () -> Void let addException: () -> Void - let removeException: (String) -> Void + let removeException: (AccountWebBrowserException) -> Void let clearExceptions: () -> Void init( @@ -32,7 +29,7 @@ private final class WebBrowserSettingsControllerArguments { clearCookies: @escaping () -> Void, clearCache: @escaping () -> Void, addException: @escaping () -> Void, - removeException: @escaping (String) -> Void, + removeException: @escaping (AccountWebBrowserException) -> Void, clearExceptions: @escaping () -> Void ) { self.context = context @@ -48,31 +45,44 @@ private final class WebBrowserSettingsControllerArguments { private enum WebBrowserSettingsSection: Int32 { case browsers case clearCookies - case exceptions + case neverExceptions + case alwaysExceptions + case clear } private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { case browserHeader(PresentationTheme, String) case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32) + case browserInfo(PresentationTheme, String) case clearCookies(PresentationTheme, String) case clearCache(PresentationTheme, String) case clearCookiesInfo(PresentationTheme, String) - case exceptionsHeader(PresentationTheme, String) - case exceptionsAdd(PresentationTheme, String) - case exception(Int32, PresentationTheme, WebBrowserException) - case exceptionsClear(PresentationTheme, String) - case exceptionsInfo(PresentationTheme, String) + case neverHeader(PresentationTheme, String) + case neverAdd(PresentationTheme, String) + case neverException(Int32, PresentationTheme, AccountWebBrowserException) + case neverExceptionsInfo(PresentationTheme, String) + case neverExceptionsClear(PresentationTheme, String) + + case alwaysHeader(PresentationTheme, String) + case alwaysAdd(PresentationTheme, String) + case alwaysException(Int32, PresentationTheme, AccountWebBrowserException) + case alwaysExceptionsInfo(PresentationTheme, String) + case alwaysExceptionsClear(PresentationTheme, String) var section: ItemListSectionId { switch self { - case .browserHeader, .browser: + case .browserHeader, .browser, .browserInfo: return WebBrowserSettingsSection.browsers.rawValue case .clearCookies, .clearCache, .clearCookiesInfo: return WebBrowserSettingsSection.clearCookies.rawValue - case .exceptionsHeader, .exceptionsAdd, .exception, .exceptionsClear, .exceptionsInfo: - return WebBrowserSettingsSection.exceptions.rawValue + case .neverHeader, .neverAdd, .neverException, .neverExceptionsInfo: + return WebBrowserSettingsSection.neverExceptions.rawValue + case .alwaysHeader, .alwaysAdd, .alwaysException, .alwaysExceptionsInfo: + return WebBrowserSettingsSection.alwaysExceptions.rawValue + case .neverExceptionsClear, .alwaysExceptionsClear: + return WebBrowserSettingsSection.clear.rawValue } } @@ -82,22 +92,34 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { return 0 case let .browser(_, _, _, _, _, index): return UInt64(1 + index) + case .browserInfo: + return 101 case .clearCookies: return 102 case .clearCache: return 103 case .clearCookiesInfo: return 104 - case .exceptionsHeader: + case .neverHeader: return 105 - case .exceptionsAdd: + case .neverAdd: return 106 - case let .exception(_, _, exception): - return 2000 + exception.domain.persistentHashValue - case .exceptionsClear: - return 1000 - case .exceptionsInfo: + case let .neverException(_, _, exception): + return 107 + exception.domain.persistentHashValue + case .neverExceptionsInfo: return 1001 + case .neverExceptionsClear: + return 1002 + case .alwaysHeader: + return 1003 + case .alwaysAdd: + return 1004 + case let .alwaysException(_, _, exception): + return 1005 + exception.domain.persistentHashValue + case .alwaysExceptionsInfo: + return 2000 + case .alwaysExceptionsClear: + return 3000 } } @@ -107,22 +129,34 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { return 0 case let .browser(_, _, _, _, _, index): return 1 + index + case .browserInfo: + return 101 case .clearCookies: return 102 case .clearCache: return 103 case .clearCookiesInfo: return 104 - case .exceptionsHeader: + case .neverHeader: return 105 - case .exceptionsAdd: + case .neverAdd: return 106 - case let .exception(index, _, _): + case let .neverException(index, _, _): return 107 + index - case .exceptionsClear: - return 1000 - case .exceptionsInfo: + case .neverExceptionsInfo: return 1001 + case .neverExceptionsClear: + return 1002 + case .alwaysHeader: + return 1003 + case .alwaysAdd: + return 1004 + case let .alwaysException(index, _, _): + return 1005 + index + case .alwaysExceptionsInfo: + return 2000 + case .alwaysExceptionsClear: + return 3000 } } @@ -140,6 +174,12 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .browserInfo(lhsTheme, lhsText): + if case let .browserInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .clearCookies(lhsTheme, lhsText): if case let .clearCookies(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -158,32 +198,62 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { } else { return false } - case let .exceptionsHeader(lhsTheme, lhsText): - if case let .exceptionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .neverHeader(lhsTheme, lhsText): + if case let .neverHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .exception(lhsIndex, lhsTheme, lhsException): - if case let .exception(rhsIndex, rhsTheme, rhsException) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsException == rhsException { + case let .neverException(lhsIndex, lhsTheme, lhsException): + if case let .neverException(rhsIndex, rhsTheme, rhsException) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsException == rhsException { return true } else { return false } - case let .exceptionsAdd(lhsTheme, lhsText): - if case let .exceptionsAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .neverAdd(lhsTheme, lhsText): + if case let .neverAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .exceptionsClear(lhsTheme, lhsText): - if case let .exceptionsClear(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .neverExceptionsInfo(lhsTheme, lhsText): + if case let .neverExceptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .exceptionsInfo(lhsTheme, lhsText): - if case let .exceptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + case let .neverExceptionsClear(lhsTheme, lhsText): + if case let .neverExceptionsClear(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .alwaysHeader(lhsTheme, lhsText): + if case let .alwaysHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .alwaysException(lhsIndex, lhsTheme, lhsException): + if case let .alwaysException(rhsIndex, rhsTheme, rhsException) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsException == rhsException { + return true + } else { + return false + } + case let .alwaysAdd(lhsTheme, lhsText): + if case let .alwaysAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .alwaysExceptionsInfo(lhsTheme, lhsText): + if case let .alwaysExceptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .alwaysExceptionsClear(lhsTheme, lhsText): + if case let .alwaysExceptionsClear(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -201,11 +271,13 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { case let .browserHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .browser(_, title, application, identifier, selected, _): - return WebBrowserItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, title: title, application: application, checked: selected, sectionId: self.section) { + return WebBrowserItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, title: title, application: application, identifier: identifier, checked: selected, sectionId: self.section) { arguments.updateDefaultBrowser(identifier) } + case let .browserInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .clearCookies(_, text): - return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.clearCookies() }) case let .clearCache(_, text): @@ -214,61 +286,93 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { }) case let .clearCookiesInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - case let .exceptionsHeader(_, text): + case let .neverHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .exception(_, _, exception): - return WebBrowserDomainExceptionItem(presentationData: presentationData, systemStyle: .glass, context: arguments.context, title: exception.title, label: exception.domain, icon: exception.icon, sectionId: self.section, style: .blocks, deleted: { - arguments.removeException(exception.domain) + case let .neverException(_, _, exception): + return WebBrowserDomainExceptionItem(presentationData: presentationData, systemStyle: .glass, context: arguments.context, title: exception.title, label: exception.domain, favicon: exception.favicon, sectionId: self.section, style: .blocks, deleted: { + arguments.removeException(exception) }) - case let .exceptionsAdd(_, text): + case let .neverAdd(_, text): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { arguments.addException() }) - case let .exceptionsClear(_, text): - return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { + case let .neverExceptionsInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .neverExceptionsClear(_, text): + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.clearExceptions() }) - case let .exceptionsInfo(_, text): + case let .alwaysHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .alwaysException(_, _, exception): + return WebBrowserDomainExceptionItem(presentationData: presentationData, systemStyle: .glass, context: arguments.context, title: exception.title, label: exception.domain, favicon: exception.favicon, sectionId: self.section, style: .blocks, deleted: { + arguments.removeException(exception) + }) + case let .alwaysAdd(_, text): + return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { + arguments.addException() + }) + case let .alwaysExceptionsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .alwaysExceptionsClear(_, text): + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + arguments.clearExceptions() + }) } } } -private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, settings: WebBrowserSettings) -> [WebBrowserSettingsControllerEntry] { +private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, localSettings: WebBrowserSettings, accountSettings: AccountWebBrowserSettings) -> [WebBrowserSettingsControllerEntry] { var entries: [WebBrowserSettingsControllerEntry] = [] - let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org")) + let options = availableOpenInOptions(context: context, item: .url(url: "https://telegram.org")) + let defaultExternalBrowser = localSettings.defaultWebBrowser ?? "default" entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title)) - entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, settings.defaultWebBrowser == nil, 0)) - + entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, !accountSettings.openExternalBrowser, 0)) + var index: Int32 = 1 for option in options { - entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == settings.defaultWebBrowser, index)) + entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, accountSettings.openExternalBrowser && option.identifier == defaultExternalBrowser, index)) index += 1 } - if settings.defaultWebBrowser == nil { - entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies)) -// entries.append(.clearCache(presentationData.theme, presentationData.strings.WebBrowser_ClearCache)) - entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info)) - - entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Title)) - entries.append(.exceptionsAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException)) + entries.append(.browserInfo(presentationData.theme, "Open links inside Telegram instead of your default browser for more privacy.")) + + entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies)) + entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info)) + + //TODO:localize + if accountSettings.openExternalBrowser { + entries.append(.neverHeader(presentationData.theme, "OPEN IN-APP")) + entries.append(.neverAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException)) var exceptionIndex: Int32 = 0 - for exception in settings.exceptions.reversed() { - entries.append(.exception(exceptionIndex, presentationData.theme, exception)) + for exception in accountSettings.inAppExceptions.reversed() { + entries.append(.neverException(exceptionIndex, presentationData.theme, exception)) exceptionIndex += 1 } + entries.append(.neverExceptionsInfo(presentationData.theme, "These sites will still be opened in-app.")) - if !settings.exceptions.isEmpty { - entries.append(.exceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Clear)) + if !accountSettings.inAppExceptions.isEmpty { + entries.append(.neverExceptionsClear(presentationData.theme, "Delete All Exceptions")) } + } else { + entries.append(.alwaysHeader(presentationData.theme, "DON'T OPEN IN-APP")) + entries.append(.alwaysAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException)) - entries.append(.exceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Info)) + var exceptionIndex: Int32 = 0 + for exception in accountSettings.externalExceptions.reversed() { + entries.append(.alwaysException(exceptionIndex, presentationData.theme, exception)) + exceptionIndex += 1 + } + entries.append(.alwaysExceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Info)) + + if !accountSettings.externalExceptions.isEmpty { + entries.append(.alwaysExceptionsClear(presentationData.theme, "Delete All Exceptions")) + } } - + return entries } @@ -276,15 +380,21 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl var clearCookiesImpl: (() -> Void)? var clearCacheImpl: (() -> Void)? var addExceptionImpl: (() -> Void)? - var removeExceptionImpl: ((String) -> Void)? + var removeExceptionImpl: ((AccountWebBrowserException) -> Void)? var clearExceptionsImpl: (() -> Void)? let arguments = WebBrowserSettingsControllerArguments( context: context, updateDefaultBrowser: { identifier in - let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { - $0.withUpdatedDefaultWebBrowser(identifier) - }).start() + let openExternalBrowser = identifier != nil + if let identifier { + let _ = (updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { + $0.withUpdatedDefaultWebBrowser(identifier) + }) + |> then(updateRemoteWebBrowserSettings(postbox: context.account.postbox, network: context.account.network, openExternalBrowser: openExternalBrowser))).start() + } else { + let _ = updateRemoteWebBrowserSettings(postbox: context.account.postbox, network: context.account.network, openExternalBrowser: openExternalBrowser).start() + } }, clearCookies: { clearCookiesImpl?() @@ -295,37 +405,39 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl addException: { addExceptionImpl?() }, - removeException: { domain in - removeExceptionImpl?(domain) + removeException: { exception in + removeExceptionImpl?(exception) }, clearExceptions: { clearExceptionsImpl?() } ) - let previousSettings = Atomic(value: nil) + let previousSettings = Atomic<(WebBrowserSettings, AccountWebBrowserSettings)?>(value: nil) let signal = combineLatest( context.sharedContext.presentationData, - context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]) + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.webBrowserSettings)) ) |> deliverOnMainQueue - |> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in - let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings - let previousSettings = previousSettings.swap(settings) + |> map { presentationData, sharedData, accountSettingsEntry -> (ItemListControllerState, (ItemListNodeState, Any)) in + let localSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings + let accountSettings = accountSettingsEntry?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + let previousSettings = previousSettings.swap((localSettings, accountSettings)) var animateChanges = false if let previousSettings { - if previousSettings.defaultWebBrowser != settings.defaultWebBrowser { + if previousSettings.0.defaultWebBrowser != localSettings.defaultWebBrowser || previousSettings.1.openExternalBrowser != accountSettings.openExternalBrowser { animateChanges = true } - if previousSettings.exceptions.count != settings.exceptions.count { + if previousSettings.1.externalExceptions.count != accountSettings.externalExceptions.count || previousSettings.1.inAppExceptions.count != accountSettings.inAppExceptions.count { animateChanges = true } } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, settings: settings), style: .blocks, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, localSettings: localSettings, accountSettings: accountSettings), style: .blocks, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } @@ -400,18 +512,13 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl var dismissImpl: (() -> Void)? let linkController = webBrowserDomainController(context: context, apply: { url in if let url { - let _ = (fetchDomainExceptionInfo(context: context, url: url) - |> deliverOnMainQueue).startStandalone(next: { newException in - let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in - var currentExceptions = currentSettings.exceptions - for exception in currentExceptions { - if exception.domain == newException.domain { - return currentSettings - } - } - currentExceptions.append(newException) - return currentSettings.withUpdatedExceptions(currentExceptions) - }).start() + let _ = (context.account.postbox.transaction { transaction -> AccountWebBrowserSettings in + return transaction.getPreferencesEntry(key: PreferencesKeys.webBrowserSettings)?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + } + |> mapToSignal { settings -> Signal in + return toggleWebBrowserSettingsException(postbox: context.account.postbox, network: context.account.network, openExternalBrowser: !settings.openExternalBrowser, delete: false, url: url) + } + |> deliverOnMainQueue).startStandalone(next: { _ in dismissImpl?() }) } @@ -423,10 +530,13 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl controller?.present(linkController, in: .window(.root)) } - removeExceptionImpl = { domain in - let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in - let updatedExceptions = currentSettings.exceptions.filter { $0.domain != domain } - return currentSettings.withUpdatedExceptions(updatedExceptions) + removeExceptionImpl = { exception in + let url = exception.url.isEmpty ? exception.domain : exception.url + let _ = (context.account.postbox.transaction { transaction -> AccountWebBrowserSettings in + return transaction.getPreferencesEntry(key: PreferencesKeys.webBrowserSettings)?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + } + |> mapToSignal { settings -> Signal in + return toggleWebBrowserSettingsException(postbox: context.account.postbox, network: context.account.network, openExternalBrowser: !settings.openExternalBrowser, delete: true, url: url) }).start() } @@ -441,9 +551,7 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Clear, action: { - let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in - return currentSettings.withUpdatedExceptions([]) - }).start() + let _ = deleteWebBrowserSettingsExceptions(postbox: context.account.postbox, network: context.account.network).start() }) ] ) @@ -452,60 +560,3 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl return controller } - -private func fetchDomainExceptionInfo(context: AccountContext, url: String) -> Signal { - let (domain, domainUrl) = cleanDomain(url: url) - if #available(iOS 13.0, *), let url = URL(string: domainUrl) { - return Signal { subscriber in - let metadataProvider = LPMetadataProvider() - metadataProvider.shouldFetchSubresources = true - metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in - let completeWithImage: (Data?) -> Void = { imageData in - var image: TelegramMediaImage? - if let imageData, let parsedImage = UIImage(data: imageData) { - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: imageData) - image = TelegramMediaImage( - imageId: EngineMedia.Id(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), - representations: [ - TelegramMediaImageRepresentation( - dimensions: PixelDimensions(width: Int32(parsedImage.size.width), height: Int32(parsedImage.size.height)), - resource: resource, - progressiveSizes: [], - immediateThumbnailData: nil, - hasVideo: false, - isPersonal: false - ) - ], - immediateThumbnailData: nil, - reference: nil, - partialReference: nil, - flags: [] - ) - } - - let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title - subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain, icon: image)) - subscriber.putCompletion() - } - - if let imageProvider = metadata?.iconProvider { - imageProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeImage as String, completionHandler: { imageUrl, _ in - guard let imageUrl, let imageData = try? Data(contentsOf: imageUrl) else { - completeWithImage(nil) - return - } - completeWithImage(imageData) - }) - } else { - completeWithImage(nil) - } - }) - return ActionDisposable { - metadataProvider.cancel() - } - } - } else { - return .single(WebBrowserException(domain: domain, title: domain, icon: nil)) - } -} diff --git a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift index d2c0a1b293..03ca221700 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountDataController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountDataController.swift @@ -315,7 +315,7 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat statePromise.get() ) |> map { presentationData, peers, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { cancelImpl() }) diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift index af79d8966e..75adcc0e24 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -408,7 +408,7 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo activeAccountsAndPeers(context: context) ) |> map { presentationData, accessChallengeData, accountsAndPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift index 631c325844..103e3af4b7 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift @@ -53,7 +53,7 @@ public class LocalizationListController: ViewController { super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) - self.editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) + self.editItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.editPressed)) self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index 1db0fbe0a9..ed9aef7f0c 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -677,7 +677,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { var listInsets = layout.insets(options: [.input]) listInsets.top += navigationBarHeight - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) listInsets.left += inset listInsets.right += inset diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift index 2759bfb669..6c97429dc4 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift @@ -43,7 +43,7 @@ public class NotificationExceptionsController: ViewController { self._hasGlassStyle = true self.removeAllItem = UIBarButtonItem(title: self.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: self, action: #selector(self.removeAllPressed)) - self.editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) + self.editItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.editPressed)) self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style @@ -98,7 +98,7 @@ public class NotificationExceptionsController: ViewController { self.controllerNode.updatePresentationData(self.presentationData) let removeAllItem = UIBarButtonItem(title: self.presentationData.strings.Notification_Exceptions_DeleteAll, style: .plain, target: self, action: #selector(self.removeAllPressed)) - let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) + let editItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.editPressed)) let doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) if self.navigationItem.rightBarButtonItem === self.editItem { self.navigationItem.rightBarButtonItem = editItem diff --git a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift index 2456dbe02e..51ffe12b41 100644 --- a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift +++ b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift @@ -1063,7 +1063,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor if !state.mode.peerIds.isEmpty { if state.editing { leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}) - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { value in return value.withUpdatedEditing(false) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift index fa1060693a..172bd2081c 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift @@ -279,7 +279,7 @@ public func blockedPeersController(context: AccountContext, blockedPeersContext: var rightNavigationButton: ItemListNavigationButton? if !blockedPeersState.peers.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift b/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift index bbc704e546..e99a3803a4 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ConfirmPhoneNumberController.swift @@ -292,7 +292,7 @@ public func confirmPhoneNumberCodeController(context: AccountContext, phoneNumbe let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue) |> deliverOnMainQueue |> map { presentationData, state, data, timeout -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) var rightNavigationButton: ItemListNavigationButton? diff --git a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift index e96125f1e7..520a405c55 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift @@ -377,7 +377,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr let signal = combineLatest(context.sharedContext.presentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) var rightNavigationButton: ItemListNavigationButton? @@ -386,7 +386,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr } else { switch state.state { case .setup: - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.passwordText.isEmpty, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: !state.passwordText.isEmpty, action: { saveImpl() }) case .pendingVerification: diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift index b656ea640d..8fa2031bbd 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift @@ -14,6 +14,7 @@ import LocalAuth import PasscodeUI import TelegramStringFormatting import TelegramIntents +import ContextUI private final class PasscodeOptionsControllerArguments { let turnPasscodeOff: () -> Void @@ -153,6 +154,18 @@ private struct PasscodeOptionsControllerState: Equatable { } } +private final class PasscodeOptionsContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0)) + } +} + private struct PasscodeOptionsData: Equatable { let accessChallenge: PostboxAccessChallengeData let presentationSettings: PresentationPasscodeSettings @@ -226,9 +239,12 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode let statePromise = ValuePromise(initialState, ignoreRepeated: true) var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var popControllerImpl: (() -> Void)? var replaceTopControllerImpl: ((ViewController, Bool) -> Void)? + var findAutolockReferenceNode: (() -> ItemListDisclosureItemNode?)? + var currentAutolockTimeout: Int32? let actionsDisposable = DisposableSet() @@ -323,8 +339,6 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode }) }, changePasscodeTimeout: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationData: presentationData) - var items: [ActionSheetItem] = [] let setAction: (Int32?) -> Void = { value in let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in @@ -342,24 +356,38 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode values.sort() #endif + var items: [ContextMenuItem] = [] for value in values { var t: Int32? if value != 0 { t = value } - items.append(ActionSheetButtonItem(title: autolockStringForTimeout(strings: presentationData.strings, timeout: t), color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - + items.append(.action(ContextMenuActionItem(text: autolockStringForTimeout(strings: presentationData.strings, timeout: t), icon: { theme in + if currentAutolockTimeout == t { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) setAction(t) - })) + }))) } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + guard let sourceNode = findAutolockReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(PasscodeOptionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) }, changeTouchId: { value in let _ = (passcodeOptionsDataPromise.get() |> take(1)).start(next: { [weak passcodeOptionsDataPromise] data in passcodeOptionsDataPromise?.set(.single(data.withUpdatedPresentationSettings(data.presentationSettings.withUpdatedEnableBiometrics(value)))) @@ -372,7 +400,8 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), passcodeOptionsDataPromise.get()) |> deliverOnMainQueue |> map { presentationData, state, passcodeOptionsData -> (ItemListControllerState, (ItemListNodeState, Any)) in - + currentAutolockTimeout = passcodeOptionsData.presentationSettings.autolockTimeout + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PasscodeSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: false) @@ -387,6 +416,9 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode controller.present(c, in: .window(.root), with: p) } } + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } @@ -396,6 +428,9 @@ func passcodeOptionsController(context: AccountContext, focusOnItemTag: Passcode replaceTopControllerImpl = { [weak controller] c, animated in (controller?.navigationController as? NavigationController)?.replaceTopController(c, animated: animated) } + findAutolockReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: PasscodeOptionsEntryTag.autolock) as? ItemListDisclosureItemNode + } if let focusOnItemTag { var didFocusOnItem = false diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index a835c6f668..4ed39ba4d5 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -13,6 +13,7 @@ import AccountContext import TelegramNotices import LocalAuth import AppBundle +import OpenInExternalAppUI import PasswordSetupUI import UndoUI import PremiumUI @@ -20,6 +21,7 @@ import AuthorizationUI import AuthenticationServices import ChatTimerScreen import PasskeysScreen +import ContextUI private final class PrivacyAndSecurityControllerArguments { let account: Account @@ -42,11 +44,12 @@ private final class PrivacyAndSecurityControllerArguments { let setupAccountAutoremove: () -> Void let setupMessageAutoremove: () -> Void let openDataSettings: () -> Void + let openBrowserSelection: () -> Void let openEmailSettings: (String?) -> Void let openMessagePrivacy: () -> Void let openGiftsPrivacy: () -> Void - init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openVoiceMessagePrivacy: @escaping () -> Void, openBioPrivacy: @escaping () -> Void, openBirthdayPrivacy: @escaping () -> Void, openSavedMusicPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openPasskeys: @escaping () -> Void, openActiveSessions: @escaping () -> Void, toggleArchiveAndMuteNonContacts: @escaping (Bool) -> Void, setupAccountAutoremove: @escaping () -> Void, setupMessageAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void, openEmailSettings: @escaping (String?) -> Void, openMessagePrivacy: @escaping () -> Void, openGiftsPrivacy: @escaping () -> Void) { + init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping () -> Void, openVoiceMessagePrivacy: @escaping () -> Void, openBioPrivacy: @escaping () -> Void, openBirthdayPrivacy: @escaping () -> Void, openSavedMusicPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVerificationAccessConfiguration?) -> Void, openPasskeys: @escaping () -> Void, openActiveSessions: @escaping () -> Void, toggleArchiveAndMuteNonContacts: @escaping (Bool) -> Void, setupAccountAutoremove: @escaping () -> Void, setupMessageAutoremove: @escaping () -> Void, openDataSettings: @escaping () -> Void, openBrowserSelection: @escaping () -> Void, openEmailSettings: @escaping (String?) -> Void, openMessagePrivacy: @escaping () -> Void, openGiftsPrivacy: @escaping () -> Void) { self.account = account self.openBlockedUsers = openBlockedUsers self.openLastSeenPrivacy = openLastSeenPrivacy @@ -67,6 +70,7 @@ private final class PrivacyAndSecurityControllerArguments { self.setupAccountAutoremove = setupAccountAutoremove self.setupMessageAutoremove = setupMessageAutoremove self.openDataSettings = openDataSettings + self.openBrowserSelection = openBrowserSelection self.openEmailSettings = openEmailSettings self.openMessagePrivacy = openMessagePrivacy self.openGiftsPrivacy = openGiftsPrivacy @@ -81,6 +85,7 @@ private enum PrivacyAndSecuritySection: Int32 { case messageAutoremove case dataSettings case loginEmail + case linkHandling } public enum PrivacyAndSecurityEntryTag: ItemListItemTag { @@ -130,6 +135,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case messageAutoremoveInfo(PresentationTheme, String) case dataSettings(PresentationTheme, String) case dataSettingsInfo(PresentationTheme, String) + case openLinksIn(PresentationTheme, String, String) var section: ItemListSectionId { switch self { @@ -145,6 +151,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return PrivacyAndSecuritySection.account.rawValue case .dataSettings, .dataSettingsInfo: return PrivacyAndSecuritySection.dataSettings.rawValue + case .openLinksIn: + return PrivacyAndSecuritySection.linkHandling.rawValue } } @@ -214,6 +222,8 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { return 31 case .dataSettingsInfo: return 32 + case .openLinksIn: + return 33 } } @@ -411,6 +421,12 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } else { return false } + case let .openLinksIn(lhsTheme, lhsText, lhsValue): + if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } } } @@ -530,6 +546,10 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { }) case let .dataSettingsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .openLinksIn(_, text, value): + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, action: { + arguments.openBrowserSelection() + }) } } } @@ -541,6 +561,18 @@ private struct PrivacyAndSecurityControllerState: Equatable { var updatingOnlyAllowPremiumNonContacts: Bool? = nil } +private final class PrivacyAndSecurityContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0)) + } +} + private func countForSelectivePeers(_ peers: [PeerId: SelectivePrivacyPeer]) -> Int { var result = 0 for (_, peer) in peers { @@ -628,6 +660,7 @@ private func privacyAndSecurityControllerEntries( isPremium: Bool, loginEmail: String?, accountPeer: EnginePeer?, + defaultWebBrowser: String, appConfiguration: AppConfiguration ) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] @@ -790,6 +823,8 @@ private func privacyAndSecurityControllerEntries( entries.append(.dataSettings(presentationData.theme, presentationData.strings.PrivacySettings_DataSettings)) entries.append(.dataSettingsInfo(presentationData.theme, presentationData.strings.PrivacySettings_DataSettingsHelp)) + entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser)) + return entries } @@ -838,7 +873,9 @@ public func privacyAndSecurityController( var pushControllerImpl: ((ViewController, Bool) -> Void)? var replaceTopControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var getNavigationControllerImpl: (() -> NavigationController?)? + var findAccountTimeoutReferenceNode: (() -> ItemListDisclosureItemNode?)? let actionsDisposable = DisposableSet() @@ -1313,12 +1350,8 @@ public func privacyAndSecurityController( |> take(1) |> deliverOnMainQueue updateAccountTimeoutDisposable.set(signal.start(next: { [weak updateAccountTimeoutDisposable] privacySettingsValue in - if let _ = privacySettingsValue { + if let privacySettingsValue = privacySettingsValue { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } let timeoutAction: (Int32) -> Void = { timeout in if let updateAccountTimeoutDisposable = updateAccountTimeoutDisposable { updateState { state in @@ -1355,15 +1388,24 @@ public func privacyAndSecurityController( 548 * 24 * 60 * 60, 730 * 24 * 60 * 60 ] - var timeoutItems: [ActionSheetItem] = timeoutValues.map { value in - return ActionSheetButtonItem(title: presentationData.strings.MessageTimer_Months(max(1, value / (60 * 60 * 24 * 30))), action: { - dismissAction() + var timeoutItems: [ContextMenuItem] = timeoutValues.map { value in + return .action(ContextMenuActionItem(text: presentationData.strings.MessageTimer_Months(max(1, value / (60 * 60 * 24 * 30))), icon: { theme in + if privacySettingsValue.accountRemovalTimeout == value { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) timeoutAction(value) - }) + })) } - timeoutItems.append(ActionSheetButtonItem(title: presentationData.strings.PrivacySettings_DeleteAccountNow, color: .destructive, action: { - dismissAction() - + timeoutItems.append(.separator) + timeoutItems.append(.action(ContextMenuActionItem(text: presentationData.strings.PrivacySettings_DeleteAccountNow, textColor: .destructive, icon: { _ in + return nil + }, action: { _, f in + f(.default) + guard let navigationController = getNavigationControllerImpl?() else { return } @@ -1374,12 +1416,22 @@ public func privacyAndSecurityController( let optionsController = deleteAccountOptionsController(context: context, navigationController: navigationController, hasTwoStepAuth: hasTwoStepAuth ?? false, twoStepAuthData: twoStepAuthData) pushControllerImpl?(optionsController, true) }) - })) - controller.setItemGroups([ - ActionSheetItemGroup(items: timeoutItems), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller) + }))) + + guard let sourceNode = findAccountTimeoutReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(PrivacyAndSecurityContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(timeoutItems))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) } })) }, setupMessageAutoremove: { @@ -1405,6 +1457,9 @@ public func privacyAndSecurityController( })) }, openDataSettings: { pushControllerImpl?(dataPrivacyController(context: context), true) + }, openBrowserSelection: { + let controller = webBrowserSettingsController(context: context) + pushControllerImpl?(controller, true) }, openEmailSettings: { emailPattern in if let emailPattern, !emailPattern.contains(" ") { let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -1524,13 +1579,18 @@ public func privacyAndSecurityController( } } + let webBrowserData = combineLatest( + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings, ApplicationSpecificSharedDataKeys.webBrowserSettings]), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.webBrowserSettings)) + ) + let signal = combineLatest( queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), privacySettingsPromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), - context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), + webBrowserData, context.engine.peers.recentPeers(), blockedPeersState.get(), webSessionsContext.state, @@ -1541,7 +1601,7 @@ public func privacyAndSecurityController( context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), loginEmail ) - |> map { presentationData, state, privacySettings, noticeView, sharedData, recentPeers, blockedPeersState, activeWebsitesState, accessChallengeData, twoStepAuth, passkeys, appConfiguration, accountPeer, loginEmail -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, privacySettings, noticeView, webBrowserData, recentPeers, blockedPeersState, activeWebsitesState, accessChallengeData, twoStepAuth, passkeys, appConfiguration, accountPeer, loginEmail -> (ItemListControllerState, (ItemListNodeState, Any)) in var canAutoarchive = false if let data = appConfiguration.data, let hasAutoarchive = data["autoarchive_setting_available"] as? Bool { canAutoarchive = hasAutoarchive @@ -1556,8 +1616,26 @@ public func privacyAndSecurityController( let isPremium = accountPeer?.isPremium ?? false let isPremiumDisabled = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }).isPremiumDisabled + let localWebBrowserSettings = webBrowserData.0.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings + let accountWebBrowserSettings = webBrowserData.1?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + + let options = availableOpenInOptions(context: context, item: .url(url: "https://telegram.org")) + let defaultWebBrowser: String + if accountWebBrowserSettings.openExternalBrowser { + var defaultExternalBrowser = localWebBrowserSettings.defaultWebBrowser + if defaultExternalBrowser == nil || defaultExternalBrowser == "inApp" || defaultExternalBrowser == "inAppSafari" { + defaultExternalBrowser = "safari" + } + if let option = options.first(where: { $0.identifier == defaultExternalBrowser }) { + defaultWebBrowser = option.title + } else { + defaultWebBrowser = "Safari" + } + } else { + defaultWebBrowser = presentationData.strings.WebBrowser_Telegram + } - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, hasTwoStepAuth: twoStepAuth.0, twoStepAuthData: twoStepAuth.1, hasPasskeys: passkeys.0, displayPasskeys: displayPasskeys, canAutoarchive: canAutoarchive, isPremiumDisabled: isPremiumDisabled, isPremium: isPremium, loginEmail: loginEmail, accountPeer: accountPeer, appConfiguration: appConfiguration), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings, accessChallengeData: accessChallengeData.data, blockedPeerCount: blockedPeersState.totalCount, activeWebsitesCount: activeWebsitesState.sessions.count, hasTwoStepAuth: twoStepAuth.0, twoStepAuthData: twoStepAuth.1, hasPasskeys: passkeys.0, displayPasskeys: displayPasskeys, canAutoarchive: canAutoarchive, isPremiumDisabled: isPremiumDisabled, isPremium: isPremium, loginEmail: loginEmail, accountPeer: accountPeer, defaultWebBrowser: defaultWebBrowser, appConfiguration: appConfiguration), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -1575,9 +1653,15 @@ public func privacyAndSecurityController( presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window(.root), with: nil) } + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } getNavigationControllerImpl = { [weak controller] in return (controller?.navigationController as? NavigationController) } + findAccountTimeoutReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: PrivacyAndSecurityEntryTag.accountTimeout) as? ItemListDisclosureItemNode + } controller.didAppear = { _ in updateHasTwoStepAuth() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift index e8847e7d57..96e61a27ee 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroControllerNode.swift @@ -166,7 +166,7 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode { let noticeSize = self.noticeNode.measure(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) let buttonInset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { buttonInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { buttonInset = 0.0 diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift index c07fe99849..757ce671e2 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListRecentSessionItem.swift @@ -34,7 +34,7 @@ enum ItemListRecentSessionItemText { case none } -final class ItemListRecentSessionItem: ListViewItem, ItemListItem { +final class ItemListRecentSessionItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem { let presentationData: ItemListPresentationData let systemStyle: ItemListSystemStyle let dateTimeFormat: PresentationDateTimeFormat @@ -47,6 +47,10 @@ final class ItemListRecentSessionItem: ListViewItem, ItemListItem { let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void let removeSession: (Int64) -> Void let action: (() -> Void)? + + var hasActiveRevealOptions: Bool { + return self.revealed + } init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editable: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) { self.presentationData = presentationData @@ -193,6 +197,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { private var layoutParams: (ItemListRecentSessionItem, ListViewItemLayoutParams, ItemListNeighbors)? private var editableControlNode: ItemListEditableControlNode? + private var isHighlighted = false override public var canBeSelected: Bool { if let item = self.layoutParams?.0, let _ = item.action { @@ -265,19 +270,12 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { return { item, params, neighbors in var updatedTheme: PresentationTheme? - let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) - let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) - - let verticalInset: CGFloat - switch item.systemStyle { - case .glass: - verticalInset = 14.0 - case .legacy: - verticalInset = 10.0 - } + let titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + let verticalInset: CGFloat = 11.0 let titleSpacing: CGFloat = 1.0 - let textSpacing: CGFloat = 3.0 + let textSpacing: CGFloat = 2.0 if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme @@ -289,7 +287,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { let peerRevealOptions: [ItemListRevealOption] if item.editable && item.enabled { - peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.AuthSessions_Terminate, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.AuthSessions_Terminate, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)] } else { peerRevealOptions = [] } @@ -307,13 +305,6 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { deviceString = item.session.deviceModel } -// if !item.session.platform.isEmpty { -// if !deviceString.isEmpty { -// deviceString += ", " -// } -// deviceString += item.session.platform -// } - var updatedIcon: UIImage? if item.session != currentItem?.session { updatedIcon = iconForSession(item.session).0 @@ -352,7 +343,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let insets = itemListNeighborsGroupedInsets(neighbors, params) - let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height) + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height - 4.0) let separatorHeight = UIScreenPixel let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0 @@ -487,26 +478,29 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false + let topStripeIsHidden: Bool switch neighbors.top { case .sameSection(false): - strongSelf.topStripeNode.isHidden = true + topStripeIsHidden = true default: hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners + topStripeIsHidden = hasCorners } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat + let bottomStripeIsHidden: Bool switch neighbors.bottom { case .sameSection(false): bottomStripeInset = leftInset + editingOffset bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false + bottomStripeIsHidden = false default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners + bottomStripeIsHidden = hasCorners } + strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil @@ -516,12 +510,12 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: 12.0), size: CGSize(width: 30.0, height: 30.0))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: 15.0), size: CGSize(width: 30.0, height: 30.0))) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset - 2.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: appLayout.size)) transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.appNode.frame.maxY + textSpacing), size: locationLayout.size)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))), transition: transition) strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) @@ -535,39 +529,8 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted && (self.layoutParams?.0.enabled ?? false) { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } + self.isHighlighted = highlighted && (self.layoutParams?.0.enabled ?? false) + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) } override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { @@ -602,6 +565,12 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.appNode.frame.minY), size: self.appNode.bounds.size)) transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.locationNode.frame.minY), size: self.locationNode.bounds.size)) } + + override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) + } override func revealOptionsInteractivelyOpened() { if let (item, _, _) = self.layoutParams { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift index 39380763fc..7e9fa8e762 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift @@ -12,6 +12,7 @@ import AvatarNode import TelegramStringFormatting import LocalizedPeerData import AccountContext +import AppBundle struct ItemListWebsiteItemEditing: Equatable { let editing: Bool @@ -28,7 +29,7 @@ struct ItemListWebsiteItemEditing: Equatable { } } -final class ItemListWebsiteItem: ListViewItem, ItemListItem { +final class ItemListWebsiteItem: ListViewItem, ItemListItem, ItemListRevealOptionsStatefulItem { let context: AccountContext let presentationData: ItemListPresentationData let systemStyle: ItemListSystemStyle @@ -43,6 +44,10 @@ final class ItemListWebsiteItem: ListViewItem, ItemListItem { let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void let removeSession: (Int64) -> Void let action: (() -> Void)? + + var hasActiveRevealOptions: Bool { + return self.revealed + } init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: EnginePeer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) { self.context = context @@ -109,7 +114,7 @@ final class ItemListWebsiteItem: ListViewItem, ItemListItem { } } -private let avatarFont = avatarPlaceholderFont(size: 11.0) +private let avatarFont = avatarPlaceholderFont(size: 13.0) private func trimmedLocationName(_ session: WebAuthorization) -> String { var country = session.region @@ -141,6 +146,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { private var layoutParams: (ItemListWebsiteItem, ListViewItemLayoutParams, ItemListNeighbors)? private var editableControlNode: ItemListEditableControlNode? + private var isHighlighted = false override public var canBeSelected: Bool { if let item = self.layoutParams?.0, let _ = item.action { @@ -213,8 +219,12 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { return { item, params, neighbors in var updatedTheme: PresentationTheme? - let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0)) - let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + let titleFont = Font.medium(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 1.0 + let textSpacing: CGFloat = 2.0 if currentItem?.presentationData !== item.presentationData.theme { updatedTheme = item.presentationData.theme @@ -224,7 +234,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { var appAttributedString: NSAttributedString? var locationAttributedString: NSAttributedString? - let peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.AuthSessions_LogOut, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] + let peerRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.AuthSessions_LogOut, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)] let rightInset: CGFloat = params.rightInset @@ -271,17 +281,12 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 5.0 - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - editingOffset - rightInset - 5.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (appLayout, appApply) = makeAppLayout(TextNodeLayoutArguments(attributedString: appAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - var verticalInset: CGFloat = 0.0 - if case .glass = item.systemStyle { - verticalInset = 4.0 - } - let insets = itemListNeighborsGroupedInsets(neighbors, params) - let contentSize = CGSize(width: params.width, height: 75.0 + verticalInset * 2.0) + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height - 4.0) let separatorHeight = UIScreenPixel let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0 @@ -410,26 +415,29 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false + let topStripeIsHidden: Bool switch neighbors.top { case .sameSection(false): - strongSelf.topStripeNode.isHidden = true + topStripeIsHidden = true default: hasTopCorners = true - strongSelf.topStripeNode.isHidden = hasCorners + topStripeIsHidden = hasCorners } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat + let bottomStripeIsHidden: Bool switch neighbors.bottom { case .sameSection(false): bottomStripeInset = leftInset + editingOffset bottomStripeOffset = -separatorHeight - strongSelf.bottomStripeNode.isHidden = false + bottomStripeIsHidden = false default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true - strongSelf.bottomStripeNode.isHidden = hasCorners + bottomStripeIsHidden = hasCorners } + strongSelf.updateRevealOptionsSeparatorNodes(top: strongSelf.topStripeNode, bottom: strongSelf.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions) strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil @@ -440,13 +448,13 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))) - transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: verticalInset + 12.0), size: CGSize(width: 30.0, height: 30.0))) + transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + 16.0, y: 15.0), size: CGSize(width: 30.0, height: 30.0))) - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 10.0), size: titleLayout.size)) - transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 30.0), size: appLayout.size)) - transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 50.0), size: locationLayout.size)) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset - 2.0), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: appLayout.size)) + transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: strongSelf.appNode.frame.maxY + textSpacing), size: locationLayout.size)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.updateRevealOptionsHighlightedBackgroundFrame(strongSelf.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))), transition: transition) strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) @@ -460,39 +468,8 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { super.setHighlighted(highlighted, at: point, animated: animated) - if highlighted && (self.layoutParams?.0.enabled ?? false) { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - var anchorNode: ASDisplayNode? - if self.bottomStripeNode.supernode != nil { - anchorNode = self.bottomStripeNode - } else if self.topStripeNode.supernode != nil { - anchorNode = self.topStripeNode - } else if self.backgroundNode.supernode != nil { - anchorNode = self.backgroundNode - } - if let anchorNode = anchorNode { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) - } else { - self.addSubnode(self.highlightedBackgroundNode) - } - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } + self.isHighlighted = highlighted && (self.layoutParams?.0.enabled ?? false) + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) } override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { @@ -527,6 +504,12 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.appNode.frame.minY), size: self.appNode.bounds.size)) transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + self.revealOffset + editingOffset, y: self.locationNode.frame.minY), size: self.locationNode.bounds.size)) } + + override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) + } override func revealOptionsInteractivelyOpened() { if let (item, _, _) = self.layoutParams { @@ -549,3 +532,376 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { } } } + +final class ItemListConnectedBotSessionItem: ListViewItem, ItemListItem { + let context: AccountContext + let presentationData: ItemListPresentationData + let systemStyle: ItemListSystemStyle + let dateTimeFormat: PresentationDateTimeFormat + let nameDisplayOrder: PresentationPersonNameOrder + let bot: TelegramAccountConnectedBot + let peer: EnginePeer? + let enabled: Bool + let sectionId: ItemListSectionId + let action: (() -> Void)? + + init( + context: AccountContext, + presentationData: ItemListPresentationData, + systemStyle: ItemListSystemStyle, + dateTimeFormat: PresentationDateTimeFormat, + nameDisplayOrder: PresentationPersonNameOrder, + bot: TelegramAccountConnectedBot, + peer: EnginePeer?, + enabled: Bool, + sectionId: ItemListSectionId, + action: (() -> Void)? + ) { + self.context = context + self.presentationData = presentationData + self.systemStyle = systemStyle + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.bot = bot + self.peer = peer + self.enabled = enabled + self.sectionId = sectionId + self.action = action + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ItemListConnectedBotSessionItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply(false) }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListConnectedBotSessionItemNode { + let makeLayout = nodeValue.asyncLayout() + + let animated: Bool + if case .None = animation { + animated = false + } else { + animated = true + } + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply(animated) + }) + } + } + } + } + } + + public var selectable: Bool = true + public func selected(listView: ListView) { + listView.clearHighlightAnimated(true) + + if self.enabled { + self.action?() + } + } +} + +private final class ItemListConnectedBotSessionItemNode: ItemListRevealOptionsItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private var disabledOverlayNode: ASDisplayNode? + private let maskNode: ASImageNode + + private let avatarNode: AvatarNode + private let titleNode: TextNode + private let appNode: TextNode + private let locationNode: TextNode + + private let containerNode: ASDisplayNode + override var controlsContainer: ASDisplayNode { + return self.containerNode + } + + private let activateArea: AccessibilityAreaNode + + private var layoutParams: (ItemListConnectedBotSessionItem, ListViewItemLayoutParams, ItemListNeighbors)? + private var isHighlighted = false + + override public var canBeSelected: Bool { + if let item = self.layoutParams?.0, let _ = item.action { + return true + } else { + return false + } + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.containerNode = ASDisplayNode() + + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.cornerRadius = 7.0 + self.avatarNode.clipsToBounds = true + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.appNode = TextNode() + self.appNode.isUserInteractionEnabled = false + self.appNode.contentMode = .left + self.appNode.contentsScale = UIScreen.main.scale + + self.locationNode = TextNode() + self.locationNode.isUserInteractionEnabled = false + self.locationNode.contentMode = .left + self.locationNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, rotated: false, seeThrough: false) + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.avatarNode) + self.containerNode.addSubnode(self.titleNode) + self.containerNode.addSubnode(self.appNode) + self.containerNode.addSubnode(self.locationNode) + + self.addSubnode(self.activateArea) + } + + func asyncLayout() -> (_ item: ItemListConnectedBotSessionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeAppLayout = TextNode.asyncLayout(self.appNode) + let makeLocationLayout = TextNode.asyncLayout(self.locationNode) + + var currentDisabledOverlayNode = self.disabledOverlayNode + let currentItem = self.layoutParams?.0 + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + + let titleFont = Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 17.0 / 17.0)) + let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 1.0 + let textSpacing: CGFloat = 2.0 + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let titleString = item.peer?.displayTitle(strings: item.presentationData.strings, displayOrder: item.nameDisplayOrder) ?? "Bot" + let titleAttributedString = NSAttributedString(string: titleString, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + + let appAttributedString = NSAttributedString( + string: item.presentationData.strings.RecentSession_ConnectedBot_Subtitle, + font: textFont, + textColor: item.presentationData.theme.list.itemPrimaryTextColor + ) + + let timeString: String + if let date = item.bot.date { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + timeString = stringForRelativeActivityTimestamp(strings: item.presentationData.strings, dateTimeFormat: item.dateTimeFormat, relativeTimestamp: date, relativeTo: timestamp) + } else { + timeString = "" + } + let locationAttributedString = NSAttributedString(string: item.presentationData.strings.RecentSessions_ConnectedBot_ConnectedTime(timeString).string, font: textFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + + let rightInset: CGFloat = params.rightInset + let leftInset: CGFloat = 59.0 + params.leftInset + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - rightInset - 5.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (appLayout, appApply) = makeAppLayout(TextNodeLayoutArguments(attributedString: appAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let insets = itemListNeighborsGroupedInsets(neighbors, params) + let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + appLayout.size.height + textSpacing + locationLayout.size.height - 4.0) + let separatorHeight = UIScreenPixel + let separatorRightInset: CGFloat = item.systemStyle == .glass ? 16.0 : 0.0 + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + if !item.enabled { + if currentDisabledOverlayNode == nil { + currentDisabledOverlayNode = ASDisplayNode() + currentDisabledOverlayNode?.backgroundColor = UIColor(white: 1.0, alpha: 0.5) + } + } else { + currentDisabledOverlayNode = nil + } + + return (layout, { [weak self] animated in + guard let self else { + return + } + + self.layoutParams = (item, params, neighbors) + + self.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + self.activateArea.accessibilityLabel = titleAttributedString.string + + var accessibilityValue = appAttributedString.string + if !locationAttributedString.string.isEmpty { + accessibilityValue += "\n\(locationAttributedString.string)" + } + self.activateArea.accessibilityValue = accessibilityValue + self.activateArea.accessibilityTraits = item.enabled ? [] : .notEnabled + + if let updatedTheme { + let _ = updatedTheme + self.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + self.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor + self.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + self.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + } + + if let peer = item.peer { + self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false) + self.avatarNode.alpha = 1.0 + } else { + self.avatarNode.alpha = 0.0 + } + + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + if let currentDisabledOverlayNode { + if currentDisabledOverlayNode != self.disabledOverlayNode { + self.disabledOverlayNode = currentDisabledOverlayNode + self.addSubnode(currentDisabledOverlayNode) + currentDisabledOverlayNode.alpha = 0.0 + transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0) + currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)) + } else { + transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))) + } + } else if let disabledOverlayNode = self.disabledOverlayNode { + transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in + disabledOverlayNode?.removeFromSupernode() + }) + self.disabledOverlayNode = nil + } + + let _ = titleApply() + let _ = appApply() + let _ = locationApply() + + if self.backgroundNode.supernode == nil { + self.insertSubnode(self.backgroundNode, at: 0) + } + if self.topStripeNode.supernode == nil { + self.insertSubnode(self.topStripeNode, at: 1) + } + if self.bottomStripeNode.supernode == nil { + self.insertSubnode(self.bottomStripeNode, at: 2) + } + if self.maskNode.supernode == nil { + self.addSubnode(self.maskNode) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + let topStripeIsHidden: Bool + switch neighbors.top { + case .sameSection(false): + topStripeIsHidden = true + default: + hasTopCorners = true + topStripeIsHidden = hasCorners + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + let bottomStripeIsHidden: Bool + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + bottomStripeOffset = -separatorHeight + bottomStripeIsHidden = false + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + hasBottomCorners = true + bottomStripeIsHidden = hasCorners + } + self.updateRevealOptionsSeparatorNodes(top: self.topStripeNode, bottom: self.bottomStripeNode, topIsHidden: topStripeIsHidden, bottomIsHidden: bottomStripeIsHidden, topHiddenByPreviousRevealOptions: neighbors.topHasActiveRevealOptions, bottomHiddenByNextRevealOptions: neighbors.bottomHasActiveRevealOptions) + + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil + + self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + self.containerNode.frame = CGRect(origin: CGPoint(), size: self.backgroundNode.frame.size) + self.maskNode.frame = self.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + transition.updateFrame(node: self.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) + transition.updateFrame(node: self.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight))) + + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 15.0), size: CGSize(width: 30.0, height: 30.0))) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset - 2.0), size: titleLayout.size)) + transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset, y: self.titleNode.frame.maxY + titleSpacing), size: appLayout.size)) + transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset, y: self.appNode.frame.maxY + textSpacing), size: locationLayout.size)) + + self.updateRevealOptionsHighlightedBackgroundFrame(self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))), transition: transition) + self.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + self.setRevealOptions((left: [], right: [])) + }) + } + } + + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + self.isHighlighted = highlighted && (self.layoutParams?.0.enabled ?? false) + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: (animated && !highlighted) ? .animated(duration: 0.4, curve: .easeInOut) : .immediate, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) + } + + override func revealOptionsActiveStateUpdated(isActive: Bool, transition: ContainedViewLayoutTransition) { + super.revealOptionsActiveStateUpdated(isActive: isActive, transition: transition) + + self.updateRevealOptionsHighlightedBackgroundNode(self.highlightedBackgroundNode, isHighlighted: self.isHighlighted || self.isRevealOptionsActive, transition: transition, aboveNodes: [self.bottomStripeNode, self.topStripeNode, self.backgroundNode]) + } + + 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/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index e13509014e..a177304ec6 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -11,6 +11,11 @@ import AccountContext import ItemListPeerActionItem import DeviceAccess import QrCodeUI +import ComponentFlow +import AlertComponent +import AlertCheckComponent +import AppBundle +import ContextUI private final class RecentSessionsControllerArguments { let context: AccountContext @@ -20,6 +25,7 @@ private final class RecentSessionsControllerArguments { let terminateOtherSessions: () -> Void let openSession: (RecentAccountSession) -> Void + let openConnectedBotSession: (TelegramAccountConnectedBot) -> Void let openWebSession: (WebAuthorization, EnginePeer?) -> Void let removeWebSession: (Int64) -> Void @@ -33,13 +39,14 @@ private final class RecentSessionsControllerArguments { let openDesktopLink: () -> Void let openWebLink: () -> Void - init(context: AccountContext, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, openSession: @escaping (RecentAccountSession) -> Void, openWebSession: @escaping (WebAuthorization, EnginePeer?) -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void, addDevice: @escaping () -> Void, openOtherAppsUrl: @escaping () -> Void, setupAuthorizationTTL: @escaping () -> Void, openDesktopLink: @escaping () -> Void, openWebLink: @escaping () -> Void) { + init(context: AccountContext, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, openSession: @escaping (RecentAccountSession) -> Void, openConnectedBotSession: @escaping (TelegramAccountConnectedBot) -> Void, openWebSession: @escaping (WebAuthorization, EnginePeer?) -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void, addDevice: @escaping () -> Void, openOtherAppsUrl: @escaping () -> Void, setupAuthorizationTTL: @escaping () -> Void, openDesktopLink: @escaping () -> Void, openWebLink: @escaping () -> Void) { self.context = context self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions self.removeSession = removeSession self.terminateOtherSessions = terminateOtherSessions self.openSession = openSession + self.openConnectedBotSession = openConnectedBotSession self.openWebSession = openWebSession self.removeWebSession = removeWebSession @@ -71,6 +78,7 @@ private enum RecentSessionsSection: Int32 { private enum RecentSessionsEntryStableId: Hashable { case session(Int64) + case connectedBot(EnginePeer.Id) case index(Int32) case devicesInfo case ttl(Int32) @@ -115,6 +123,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry { case pendingSessionsInfo(SortIndex, String) case otherSessionsHeader(SortIndex, String) case addDevice(SortIndex, String) + case connectedBot(sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, bot: TelegramAccountConnectedBot, peer: EnginePeer?, enabled: Bool) case session(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) case website(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: EnginePeer?, enabled: Bool, editing: Bool, revealed: Bool) case devicesInfo(SortIndex, String) @@ -129,7 +138,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry { return RecentSessionsSection.currentSession.rawValue case .pendingSessionsHeader, .pendingSession, .pendingSessionsInfo: return RecentSessionsSection.pendingSessions.rawValue - case .otherSessionsHeader, .addDevice, .session, .website, .devicesInfo: + case .otherSessionsHeader, .addDevice, .connectedBot, .session, .website, .devicesInfo: return RecentSessionsSection.otherSessions.rawValue case .ttlHeader, .ttlTimeout: return RecentSessionsSection.ttl.rawValue @@ -162,6 +171,8 @@ private enum RecentSessionsEntry: ItemListNodeEntry { return .index(9) case .addDevice: return .index(10) + case let .connectedBot(_, _, _, _, bot, _, _): + return .connectedBot(bot.id) case let .session(_, _, _, _, session, _, _, _): return .session(session.hash) case let .website(_, _, _, _, _, website, _, _, _, _): @@ -201,6 +212,8 @@ private enum RecentSessionsEntry: ItemListNodeEntry { return index case let .addDevice(index, _): return index + case let .connectedBot(index, _, _, _, _, _, _): + return index case let .session(_, index, _, _, _, _, _, _): return index case let .website(_, index, _, _, _, _, _, _, _, _): @@ -282,6 +295,12 @@ private enum RecentSessionsEntry: ItemListNodeEntry { } else { return false } + case let .connectedBot(lhsSortIndex, lhsStrings, lhsDateTimeFormat, lhsNameDisplayOrder, lhsBot, lhsPeer, lhsEnabled): + if case let .connectedBot(rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsNameDisplayOrder, rhsBot, rhsPeer, rhsEnabled) = rhs, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsBot == rhsBot, lhsPeer == rhsPeer, lhsEnabled == rhsEnabled { + return true + } else { + return false + } case let .currentSession(lhsSortIndex, lhsStrings, lhsDateTimeFormat, lhsSession): if case let .currentSession(rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsSession) = rhs, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession { return true @@ -388,6 +407,21 @@ private enum RecentSessionsEntry: ItemListNodeEntry { return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.addDeviceIcon(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { arguments.addDevice() }) + case let .connectedBot(_, _, dateTimeFormat, nameDisplayOrder, bot, peer, enabled): + return ItemListConnectedBotSessionItem( + context: arguments.context, + presentationData: presentationData, + systemStyle: .glass, + dateTimeFormat: dateTimeFormat, + nameDisplayOrder: nameDisplayOrder, + bot: bot, + peer: peer, + enabled: enabled, + sectionId: self.section, + action: { + arguments.openConnectedBotSession(bot) + } + ) case let .session(_, _, _, dateTimeFormat, session, enabled, editing, revealed): return ItemListRecentSessionItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in arguments.setSessionIdWithRevealedOptions(previousId, id) @@ -475,7 +509,7 @@ private struct RecentSessionsControllerState: Equatable { } } -private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, sessionsState: ActiveSessionsContextState, enableQRLogin: Bool) -> [RecentSessionsEntry] { +private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, sessionsState: ActiveSessionsContextState, connectedBot: TelegramAccountConnectedBot?, connectedBotPeer: EnginePeer?, enableQRLogin: Bool) -> [RecentSessionsEntry] { var entries: [RecentSessionsEntry] = [] entries.append(.header(SortIndex(section: 0, item: 0), presentationData.strings.AuthSessions_HeaderInfo)) @@ -483,16 +517,19 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, if !sessionsState.sessions.isEmpty { var existingSessionIds = Set() entries.append(.currentSessionHeader(SortIndex(section: 1, item: 0), presentationData.strings.AuthSessions_CurrentSession)) + var currentSessionItemIndex = 1 if let index = sessionsState.sessions.firstIndex(where: { $0.hash == 0 }) { existingSessionIds.insert(sessionsState.sessions[index].hash) - entries.append(.currentSession(SortIndex(section: 1, item: 1), presentationData.strings, presentationData.dateTimeFormat, sessionsState.sessions[index])) + entries.append(.currentSession(SortIndex(section: 1, item: currentSessionItemIndex), presentationData.strings, presentationData.dateTimeFormat, sessionsState.sessions[index])) + currentSessionItemIndex += 1 } var hasAddDevice = false - if sessionsState.sessions.count > 1 || enableQRLogin { - if sessionsState.sessions.count > 1 { - entries.append(.terminateOtherSessions(SortIndex(section: 1, item: 2), presentationData.strings.AuthSessions_TerminateOtherSessions)) - entries.append(.currentSessionInfo(SortIndex(section: 1, item: 3), presentationData.strings.AuthSessions_TerminateOtherSessionsHelp)) + if sessionsState.sessions.count > 1 || enableQRLogin || connectedBot != nil { + if sessionsState.sessions.count > 1 || connectedBot != nil { + entries.append(.terminateOtherSessions(SortIndex(section: 1, item: currentSessionItemIndex), presentationData.strings.AuthSessions_TerminateOtherSessions)) + currentSessionItemIndex += 1 + entries.append(.currentSessionInfo(SortIndex(section: 1, item: currentSessionItemIndex), presentationData.strings.AuthSessions_TerminateOtherSessionsHelp)) } else if enableQRLogin { hasAddDevice = true // entries.append(.currentAddDevice(SortIndex(section: 1, item: 4), presentationData.strings.AuthSessions_AddDevice)) @@ -511,7 +548,7 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, entries.append(.pendingSessionsInfo(SortIndex(section: 3, item: 0), presentationData.strings.AuthSessions_IncompleteAttemptsInfo)) } - if sessionsState.sessions.count > 1 { + if sessionsState.sessions.count > 1 || connectedBot != nil { entries.append(.otherSessionsHeader(SortIndex(section: 4, item: 0), presentationData.strings.AuthSessions_OtherSessions)) } @@ -523,10 +560,16 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, return lhs.activityDate > rhs.activityDate }) + var otherSessionItemIndex = 0 + if let connectedBot { + entries.append(.connectedBot(sortIndex: SortIndex(section: 5, item: otherSessionItemIndex), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, bot: connectedBot, peer: connectedBotPeer, enabled: !state.terminatingOtherSessions)) + otherSessionItemIndex += 1 + } for i in 0 ..< filteredSessions.count { if !existingSessionIds.contains(filteredSessions[i].hash) { existingSessionIds.insert(filteredSessions[i].hash) - entries.append(.session(index: Int32(i), sortIndex: SortIndex(section: 5, item: i), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, session: filteredSessions[i], enabled: state.removingSessionId != filteredSessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == filteredSessions[i].hash)) + entries.append(.session(index: Int32(i), sortIndex: SortIndex(section: 5, item: otherSessionItemIndex), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, session: filteredSessions[i], enabled: state.removingSessionId != filteredSessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == filteredSessions[i].hash)) + otherSessionItemIndex += 1 } } @@ -573,6 +616,18 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, private final class RecentSessionsControllerImpl: ItemListController, RecentSessionsController { } +private final class RecentSessionsContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0)) + } +} + public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext, websitesOnly: Bool, focusOnItemTag: RecentSessionsEntryTag? = nil) -> ViewController & RecentSessionsController { let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: RecentSessionsControllerState()) @@ -590,8 +645,11 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont webSessionsContext.loadMore() var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? + var findAutoTerminateReferenceNode: (() -> ItemListDisclosureItemNode?)? + var currentAuthorizationTTLDays: Int32? let actionsDisposable = DisposableSet() @@ -675,6 +733,8 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont let updateSessionDisposable = MetaDisposable() actionsDisposable.add(updateSessionDisposable) + let connectedBotAndPeerValue = Atomic<(TelegramAccountConnectedBot?, EnginePeer?)>(value: (nil, nil)) + let arguments = RecentSessionsControllerArguments(context: context, setSessionIdWithRevealedOptions: { sessionId, fromSessionId in updateState { state in if (sessionId == nil && fromSessionId == state.sessionIdWithRevealedOptions) || (sessionId != nil && fromSessionId == nil) { @@ -687,33 +747,98 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont removeSessionImpl(sessionId, {}) }, terminateOtherSessions: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = textAlertController( - context: context, - title: nil, - text: presentationData.strings.AuthSessions_TerminateOtherSessionsText, - actions: [ - TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.AuthSessions_TerminateOtherSessions, action: { - updateState { - return $0.withUpdatedTerminatingOtherSessions(true) - } - - terminateOtherSessionsDisposable.set((activeSessionsContext.removeOther() - |> deliverOnMainQueue).start(error: { _ in - updateState { - return $0.withUpdatedTerminatingOtherSessions(false) + let performTerminate: (Bool) -> Void = { terminateBot in + updateState { + return $0.withUpdatedTerminatingOtherSessions(true) + } + + let signal: Signal + if terminateBot { + signal = activeSessionsContext.removeOther() + |> then(context.engine.accountData.setAccountConnectedBot(bot: nil) |> castError(TerminateSessionError.self)) + } else { + signal = activeSessionsContext.removeOther() + } + + terminateOtherSessionsDisposable.set((signal + |> deliverOnMainQueue).start(error: { _ in + updateState { + return $0.withUpdatedTerminatingOtherSessions(false) + } + }, completed: { + updateState { + return $0.withUpdatedTerminatingOtherSessions(false) + } + context.sharedContext.updateNotificationTokensRegistration() + })) + } + + let (connectedBot, connectedBotPeer) = connectedBotAndPeerValue.with { $0 } + if connectedBot != nil { + let checkState = AlertCheckComponent.ExternalState() + let botUsername = connectedBotPeer?.addressName.flatMap { addressName -> String? in + if addressName.isEmpty { + return nil + } else { + return "@\(addressName)" + } + } ?? connectedBotPeer?.compactDisplayTitle ?? "" + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain(presentationData.strings.AuthSessions_TerminateOtherSessionsText)) + ) + )) + var controller: AlertScreen? + content.append(AnyComponentWithIdentity( + id: "check", + component: AnyComponent( + AlertCheckComponent(title: presentationData.strings.RecentSessions_ConnectedBot_TerminateCheckbox(botUsername.isEmpty ? "" : "[\(botUsername)](peer)").string, initialValue: false, externalState: checkState, linkAction: { + guard let connectedBotPeer, let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: connectedBotPeer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return } - }, completed: { - updateState { - return $0.withUpdatedTerminatingOtherSessions(false) + if let controller { + controller.dismiss(completion: { + pushControllerImpl?(infoController) + }) + } else { + pushControllerImpl?(infoController) } - context.sharedContext.updateNotificationTokensRegistration() - })) - }), - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) - ], - actionLayout: .vertical - ) - presentControllerImpl?(controller, nil) + }) + ) + )) + + controller = AlertScreen( + configuration: AlertScreen.Configuration(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: presentationData.strings.AuthSessions_TerminateOtherSessions, type: .defaultDestructive, action: { + performTerminate(checkState.value) + }), + .init(title: presentationData.strings.Common_Cancel) + ], + updatedPresentationData: (presentationData, context.sharedContext.presentationData) + ) + if let controller { + presentControllerImpl?(controller, nil) + } + } else { + let controller = textAlertController( + context: context, + title: nil, + text: presentationData.strings.AuthSessions_TerminateOtherSessionsText, + actions: [ + TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.AuthSessions_TerminateOtherSessions, action: { + performTerminate(false) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical + ) + presentControllerImpl?(controller, nil) + } }, openSession: { session in let controller = RecentSessionScreen(context: context, subject: .session(session), updateAcceptSecretChats: { value in updateSessionDisposable.set(activeSessionsContext.updateSessionAcceptsSecretChats(session, accepts: value).start()) @@ -724,13 +849,20 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont completion() }) }) - presentControllerImpl?(controller, nil) + pushControllerImpl?(controller) + }, openConnectedBotSession: { bot in + let controller = RecentSessionScreen(context: context, subject: .connectedBot(bot), updateAcceptSecretChats: { _ in + }, updateAcceptIncomingCalls: { _ in + }, remove: { completion in + completion() + }) + pushControllerImpl?(controller) }, openWebSession: { session, peer in let controller = RecentSessionScreen(context: context, subject: .website(session, peer), updateAcceptSecretChats: { _ in }, updateAcceptIncomingCalls: { _ in }, remove: { completion in removeWebSessionImpl(session.hash) completion() }) - presentControllerImpl?(controller, nil) + pushControllerImpl?(controller) }, removeWebSession: { sessionId in removeWebSessionImpl(sessionId) }, terminateAllWebSessions: { @@ -778,10 +910,6 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://telegram.org/apps", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }, setupAuthorizationTTL: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } let ttlAction: (Int32) -> Void = { ttl in updateAuthorizationTTLDisposable.set(activeSessionsContext.updateAuthorizationTTL(days: ttl).start()) } @@ -791,17 +919,32 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont 90, 180 ] - let timeoutItems: [ActionSheetItem] = timeoutValues.map { value in - return ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: value * 24 * 60 * 60), action: { - dismissAction() + let timeoutItems: [ContextMenuItem] = timeoutValues.map { value in + return .action(ContextMenuActionItem(text: timeIntervalString(strings: presentationData.strings, value: value * 24 * 60 * 60), icon: { theme in + if currentAuthorizationTTLDays == value { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) ttlAction(value) - }) + })) } - controller.setItemGroups([ - ActionSheetItemGroup(items: timeoutItems), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, nil) + guard let sourceNode = findAutoTerminateReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(RecentSessionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(timeoutItems))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) }, openDesktopLink: { context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://getdesktop.telegram.org", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }, openWebLink: { @@ -822,9 +965,29 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } |> distinctUntilChanged - let signal = combineLatest(context.sharedContext.presentationData, mode.get(), statePromise.get(), activeSessionsContext.state, webSessionsContext.state, enableQRLogin) + let connectedBotAndPeer = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.BusinessConnectedBot(id: context.account.peerId) + ) + |> mapToSignal { connectedBot -> Signal<(TelegramAccountConnectedBot?, EnginePeer?), NoError> in + guard let connectedBot else { + return .single((nil, nil)) + } + return context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: connectedBot.id) + ) + |> map { peer in + return (connectedBot, peer) + } + } + |> afterNext { value in + let _ = connectedBotAndPeerValue.swap(value) + } + + let signal = combineLatest(context.sharedContext.presentationData, mode.get(), statePromise.get(), activeSessionsContext.state, webSessionsContext.state, enableQRLogin, connectedBotAndPeer) |> deliverOnMainQueue - |> map { presentationData, mode, state, sessionsState, websitesAndPeers, enableQRLogin -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, mode, state, sessionsState, websitesAndPeers, enableQRLogin, connectedBotAndPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in + currentAuthorizationTTLDays = sessionsState.ttlDays + var rightNavigationButton: ItemListNavigationButton? let websites = websitesAndPeers.sessions let peers = websitesAndPeers.peers @@ -833,7 +996,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont if state.terminatingOtherSessions { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else if state.editing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } @@ -848,11 +1011,6 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } let emptyStateItem: ItemListControllerEmptyStateItem? = nil -// if sessionsState.sessions.count == 1 && mode == .sessions { -// emptyStateItem = RecentSessionsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings) -// } else { -// emptyStateItem = nil -// } let title: ItemListControllerTitle let entries: [RecentSessionsEntry] @@ -867,7 +1025,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont case (.websites, let websites, let peers): entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, websites: websites, peers: peers) default: - entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, sessionsState: sessionsState, enableQRLogin: enableQRLogin) + entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, sessionsState: sessionsState, connectedBot: connectedBotAndPeer.0, connectedBotPeer: connectedBotAndPeer.1, enableQRLogin: enableQRLogin) } let previousMode = previousMode.swap(mode) @@ -898,12 +1056,18 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont controller.present(c, in: .window(.root), with: p) } } + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } pushControllerImpl = { [weak controller] c in controller?.push(c) } dismissImpl = { [weak controller] in controller?.dismiss() } + findAutoTerminateReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: RecentSessionsEntryTag.autoTerminate) as? ItemListDisclosureItemNode + } if let focusOnItemTag { var didFocusOnItem = false diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift index 0b9dab7e70..2b6d4b53cd 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsHeaderItem.swift @@ -72,7 +72,7 @@ class RecentSessionsHeaderItem: ListViewItem, ItemListItem { } } -private let titleFont = Font.regular(13.0) +private let titleFont = Font.regular(14.0) class RecentSessionsHeaderItemNode: ListViewItemNode { private let titleNode: TextNode @@ -129,11 +129,11 @@ class RecentSessionsHeaderItemNode: ListViewItemNode { updatedTheme = item.theme } - let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in + let attributedText = parseMarkdownIntoAttributedString(item.text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) })) - 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 (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, lineSpacing: 0.2, cutout: nil, insets: UIEdgeInsets())) let contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + 69.0) let insets = itemListNeighborsGroupedInsets(neighbors, params) @@ -158,7 +158,7 @@ class RecentSessionsHeaderItemNode: ListViewItemNode { strongSelf.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: item.theme)) } - let buttonSideInset: CGFloat = 36.0 + let buttonSideInset: CGFloat = 16.0 let buttonWidth = min(375, contentSize.width - buttonSideInset * 2.0) let buttonHeight = 52.0 let buttonFrame = CGRect(x: floorToScreenPixels((params.width - buttonWidth) / 2.0), y: contentSize.height - buttonHeight + 4.0, width: buttonWidth, height: buttonHeight) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift index 8ba28212a7..65ffcd5549 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift @@ -8,7 +8,7 @@ import AccountContext import TelegramPresentationData import ComponentFlow import ViewControllerComponent -import SheetComponent +import ResizableSheetComponent import MultilineTextComponent import BalancedTextComponent import GlassBarButtonComponent @@ -22,6 +22,39 @@ import ListActionItemComponent import AvatarComponent import TelegramStringFormatting import Markdown +import AppBundle +import TextFormat +import ChatbotSetupScreen + +private func botRecipientsCategoryCount(_ categories: TelegramBusinessRecipients.Categories) -> Int { + var count = 0 + if categories.contains(.existingChats) { + count += 1 + } + if categories.contains(.newChats) { + count += 1 + } + if categories.contains(.contacts) { + count += 1 + } + if categories.contains(.nonContacts) { + count += 1 + } + return count +} + +private let recentSessionCheckIcon: UIImage = { + return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(1.98) + context.setLineCap(.round) + context.setLineJoin(.round) + context.translateBy(x: 1.0, y: 1.0) + + let _ = try? drawSvgPath(context, path: "M0.215053763,4.36080467 L3.31621263,7.70466293 L3.31621263,7.70466293 C3.35339229,7.74475231 3.41603123,7.74711109 3.45612061,7.70993143 C3.45920681,7.70706923 3.46210733,7.70401312 3.46480451,7.70078171 L9.89247312,0 S ") + })!.withRenderingMode(.alwaysTemplate) +}() private final class RecentSessionSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -48,12 +81,28 @@ private final class RecentSessionSheetContent: CombinedComponent { } final class State: ComponentState { + fileprivate struct ConnectedBotRecipientsState { + var categories: TelegramBusinessRecipients.Categories + var peers: [BusinessRecipientListScreen.ResolvedPeer] + var excludePeers: [BusinessRecipientListScreen.ResolvedPeer] + var excludeByDefault: Bool + } + + private let context: AccountContext + private let subject: RecentSessionScreen.Subject + private let botPeerDisposable = MetaDisposable() + var allowSecretChats: Bool? var allowIncomingCalls: Bool? + var botPeer: EnginePeer? + fileprivate var connectedBotRecipients: ConnectedBotRecipientsState? weak var controller: RecentSessionScreen? - init(subject: RecentSessionScreen.Subject) { + init(context: AccountContext, subject: RecentSessionScreen.Subject) { + self.context = context + self.subject = subject + super.init() switch subject { @@ -67,9 +116,61 @@ private final class RecentSessionSheetContent: CombinedComponent { } case .website: break + case let .connectedBot(connectedBot): + var additionalPeerIds = Set() + additionalPeerIds.formUnion(connectedBot.recipients.additionalPeers) + additionalPeerIds.formUnion(connectedBot.recipients.excludePeers) + + self.botPeerDisposable.set(( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: connectedBot.id), + EngineDataMap(additionalPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))), + EngineDataMap(additionalPeerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:))) + ) + |> deliverOnMainQueue + ).start(next: { [weak self] peer, peers, isContacts in + guard let self else { + return + } + self.botPeer = peer + + var additionalPeers: [BusinessRecipientListScreen.ResolvedPeer] = [] + for peerId in connectedBot.recipients.additionalPeers.sorted() { + guard let maybePeer = peers[peerId], let peer = maybePeer else { + continue + } + additionalPeers.append(BusinessRecipientListScreen.ResolvedPeer( + peer: peer, + isContact: isContacts[peerId] ?? false + )) + } + + var excludePeers: [BusinessRecipientListScreen.ResolvedPeer] = [] + for peerId in connectedBot.recipients.excludePeers.sorted() { + guard let maybePeer = peers[peerId], let peer = maybePeer else { + continue + } + excludePeers.append(BusinessRecipientListScreen.ResolvedPeer( + peer: peer, + isContact: isContacts[peerId] ?? false + )) + } + + self.connectedBotRecipients = ConnectedBotRecipientsState( + categories: connectedBot.recipients.categories, + peers: additionalPeers, + excludePeers: excludePeers, + excludeByDefault: connectedBot.recipients.exclude + ) + self.updated() + })) } } + deinit { + self.botPeerDisposable.dispose() + } + func toggleAllowSecretChats() { guard let controller = self.controller else { return @@ -103,31 +204,96 @@ private final class RecentSessionSheetContent: CombinedComponent { return } self.updated() + controller.terminate() + } + + private func applyConnectedBotRecipientsUpdate() { + guard case let .connectedBot(connectedBot) = self.subject, let connectedBotRecipients = self.connectedBotRecipients else { + return + } - controller.remove({ [weak controller] in - controller?.dismissAnimated() - }) + let recipients = TelegramBusinessRecipients( + categories: connectedBotRecipients.categories, + additionalPeers: Set(connectedBotRecipients.peers.map(\.peer.id)), + excludePeers: Set(connectedBotRecipients.excludePeers.map(\.peer.id)), + exclude: connectedBotRecipients.excludeByDefault + ) + let _ = self.context.engine.accountData.setAccountConnectedBot(bot: TelegramAccountConnectedBot( + id: connectedBot.id, + recipients: recipients, + rights: connectedBot.rights, + device: connectedBot.device, + date: connectedBot.date, + location: connectedBot.location + )).startStandalone() + } + + func setConnectedBotAccessMode(excludeByDefault: Bool) { + guard var connectedBotRecipients = self.connectedBotRecipients else { + return + } + guard connectedBotRecipients.excludeByDefault != excludeByDefault else { + return + } + connectedBotRecipients.excludeByDefault = excludeByDefault + connectedBotRecipients.categories = [] //removeAll() + connectedBotRecipients.peers.removeAll() + self.connectedBotRecipients = connectedBotRecipients + self.applyConnectedBotRecipientsUpdate() + self.updated() + } + + func openConnectedBotRecipients(isExclude: Bool) { + guard let controller = self.controller, let connectedBotRecipients = self.connectedBotRecipients else { + return + } + + BusinessRecipientListScreen.openSetupFlow( + context: self.context, + from: controller, + state: BusinessRecipientListScreen.PeerListState( + categories: connectedBotRecipients.categories, + peers: connectedBotRecipients.peers, + excludePeers: connectedBotRecipients.excludePeers, + excludeByDefault: connectedBotRecipients.excludeByDefault + ), + isExclude: isExclude, + update: { [weak self] updatedState in + guard let self else { + return + } + self.connectedBotRecipients = ConnectedBotRecipientsState( + categories: updatedState.categories, + peers: updatedState.peers, + excludePeers: updatedState.excludePeers, + excludeByDefault: updatedState.excludeByDefault + ) + self.applyConnectedBotRecipientsUpdate() + self.updated() + } + ) } } func makeState() -> State { - return State(subject: self.subject) + return State(context: self.context, subject: self.subject) } static var body: Body { - let closeButton = Child(GlassBarButtonComponent.self) let icon = Child(ZStack.self) let avatar = Child(AvatarComponent.self) let title = Child(BalancedTextComponent.self) let description = Child(MultilineTextComponent.self) + let recipientsModeSection = Child(ListSectionComponent.self) + let recipientsSummarySection = Child(ListSectionComponent.self) + let recipientsExcludedSection = Child(ListSectionComponent.self) let clientSection = Child(ListSectionComponent.self) let optionsSection = Child(ListSectionComponent.self) - let button = Child(ButtonComponent.self) return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component - let theme = environment.theme + let theme = environment.theme.withModalBlocksBackground() let strings = environment.strings let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let state = context.state @@ -137,29 +303,6 @@ private final class RecentSessionSheetContent: CombinedComponent { let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let closeButton = closeButton.update( - 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.cancel(true) - } - ), - availableSize: CGSize(width: 44.0, height: 44.0), - transition: .immediate - ) - context.add(closeButton - .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) - ) - var contentHeight: CGFloat = 32.0 switch component.subject { case let .session(session): @@ -223,7 +366,25 @@ private final class RecentSessionSheetContent: CombinedComponent { let avatar = avatar.update( component: AvatarComponent( context: component.context, - theme: environment.theme, + theme: theme, + peer: peer, + clipStyle: .roundedRect + ), + availableSize: CGSize(width: 92.0, height: 92.0), + transition: .immediate + ) + context.add(avatar + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + avatar.size.height / 2.0)) + ) + contentHeight += avatar.size.height + contentHeight += 18.0 + } + case .connectedBot: + if let peer = state.botPeer { + let avatar = avatar.update( + component: AvatarComponent( + context: component.context, + theme: theme, peer: peer, clipStyle: .roundedRect ), @@ -239,37 +400,54 @@ private final class RecentSessionSheetContent: CombinedComponent { } let titleString: String - let subtitleString: String - let subtitleActive: Bool + let subtitleText: MultilineTextComponent.TextContent + let subtitleHighlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? + let subtitleTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? let applicationTitle: String let applicationString: String let ipString: String? + let dateString: String? let locationString: String let buttonString: String? + let clientSectionHeader: String? + let clientSectionFooter: String? + let connectedBot: TelegramAccountConnectedBot? + + let openBotProfile: () -> Void = { + guard let botPeer = state.botPeer, let navigationController = environment.controller()?.navigationController as? NavigationController, let peerInfoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: botPeer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + navigationController.pushViewController(peerInfoController) + } switch component.subject { case let .session(session): titleString = session.deviceModel if session.isCurrent { - subtitleString = strings.Presence_online - subtitleActive = true + subtitleText = .plain(NSAttributedString(string: strings.Presence_online, font: Font.regular(15.0), textColor: theme.actionSheet.controlAccentColor)) } else { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - subtitleString = stringForRelativeActivityTimestamp(strings: strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: session.activityDate, relativeTo: timestamp) - subtitleActive = false + subtitleText = .plain(NSAttributedString(string: stringForRelativeActivityTimestamp(strings: strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: session.activityDate, relativeTo: timestamp), font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor)) } + subtitleHighlightAction = nil + subtitleTapAction = nil var appVersion = session.appVersion appVersion = appVersion.replacingOccurrences(of: "APPSTORE", with: "").replacingOccurrences(of: "BETA", with: "Beta").trimmingTrailingSpaces() applicationTitle = strings.AuthSessions_View_Application applicationString = "\(session.appName) \(appVersion)" ipString = nil + dateString = nil locationString = session.country buttonString = !session.isCurrent ? strings.AuthSessions_View_TerminateSession : nil + clientSectionHeader = nil + clientSectionFooter = strings.AuthSessions_View_LocationInfo + connectedBot = nil case let .website(website, peer): titleString = peer?.compactDisplayTitle ?? "" - subtitleString = website.domain - subtitleActive = false + subtitleText = .plain(NSAttributedString(string: website.domain, font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor)) + subtitleHighlightAction = nil + subtitleTapAction = nil var deviceString = "" if !website.browser.isEmpty { @@ -284,9 +462,68 @@ private final class RecentSessionSheetContent: CombinedComponent { applicationTitle = strings.AuthSessions_View_Browser applicationString = deviceString ipString = website.ip + dateString = nil locationString = website.region buttonString = strings.AuthSessions_View_Logout + clientSectionHeader = nil + clientSectionFooter = strings.AuthSessions_View_LocationInfo + connectedBot = nil + case let .connectedBot(subjectConnectedBot): + let botUsername = state.botPeer?.addressName.flatMap { addressName -> String? in + if addressName.isEmpty { + return nil + } else { + return "@\(addressName)" + } + } ?? "" + titleString = state.botPeer?.compactDisplayTitle ?? "Bot" + + let subtitleString = strings.RecentSession_ConnectedBot_Subtitle + if botUsername.isEmpty { + subtitleText = .plain(NSAttributedString(string: subtitleString, font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor)) + subtitleHighlightAction = nil + subtitleTapAction = nil + } else { + subtitleText = .markdown( + text: "\(subtitleString)\n[\(botUsername)](peer)", + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor), + bold: MarkdownAttributeSet(font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: theme.actionSheet.controlAccentColor), + linkAttribute: { _ in + return (TelegramTextAttributes.URL, "peer") + } + ) + ) + subtitleHighlightAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + } + subtitleTapAction = { attributes, _ in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + openBotProfile() + } + } + } + + applicationTitle = strings.RecentSession_ConnectedBot_Device + applicationString = subjectConnectedBot.device ?? "" + ipString = nil + if let date = subjectConnectedBot.date { + dateString = humanReadableStringForTimestamp(strings: strings, dateTimeFormat: presentationData.dateTimeFormat, timestamp: date, alwaysShowTime: true, allowYesterday: true).string + } else { + dateString = "" + } + locationString = subjectConnectedBot.location ?? "" + + buttonString = strings.AuthSessions_View_TerminateSession + clientSectionHeader = strings.RecentSession_ConnectedBot_ConnectedFrom + clientSectionFooter = nil + connectedBot = subjectConnectedBot } let titleFont = Font.bold(24.0) @@ -305,13 +542,15 @@ private final class RecentSessionSheetContent: CombinedComponent { contentHeight += title.size.height contentHeight += 2.0 - let textFont = Font.regular(15.0) let description = description.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: subtitleString, font: textFont, textColor: subtitleActive ? theme.actionSheet.controlAccentColor : theme.actionSheet.secondaryTextColor)), + text: subtitleText, horizontalAlignment: .center, maximumNumberOfLines: 3, - lineSpacing: 0.2 + lineSpacing: 0.2, + highlightColor: theme.actionSheet.controlAccentColor.withAlphaComponent(0.1), + highlightAction: subtitleHighlightAction, + tapAction: subtitleTapAction ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate @@ -424,19 +663,65 @@ private final class RecentSessionSheetContent: CombinedComponent { )) ) + if let dateString { + clientSectionItems.append( + AnyComponentWithIdentity(id: "date", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.RecentSession_ConnectedBot_Date, + font: Font.regular(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "info", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: dateString, + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: false + )), + action: nil + ) + )) + ) + } + let clientSection = clientSection.update( component: ListSectionComponent( theme: theme, style: .glass, - header: nil, - footer: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: strings.AuthSessions_View_LocationInfo, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )), + header: clientSectionHeader.flatMap { header in + AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: header, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )) + }, + footer: clientSectionFooter.flatMap { footer in + AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: footer, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )) + }, items: clientSectionItems ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), @@ -447,6 +732,227 @@ private final class RecentSessionSheetContent: CombinedComponent { ) contentHeight += clientSection.size.height + if let connectedBot { + contentHeight += 32.0 + + let recipientCategories = state.connectedBotRecipients?.categories ?? connectedBot.recipients.categories + let hasAccessToAllChatsByDefault = state.connectedBotRecipients?.excludeByDefault ?? connectedBot.recipients.exclude + let categoriesAndUsersItemCount = botRecipientsCategoryCount(recipientCategories) + (state.connectedBotRecipients?.peers.count ?? connectedBot.recipients.additionalPeers.count) + let excludedSectionValue: String + if categoriesAndUsersItemCount == 0 { + excludedSectionValue = strings.ChatbotSetup_RecipientSummary_ValueEmpty + } else { + excludedSectionValue = strings.ChatbotSetup_RecipientSummary_ValueItems(Int32(categoriesAndUsersItemCount)) + } + + let excludedUsersItemCount = state.connectedBotRecipients?.excludePeers.count ?? connectedBot.recipients.excludePeers.count + let excludedUsersValue: String + if excludedUsersItemCount == 0 { + excludedUsersValue = strings.ChatbotSetup_RecipientSummary_ValueEmpty + } else { + excludedUsersValue = strings.ChatbotSetup_RecipientSummary_ValueItems(Int32(excludedUsersItemCount)) + } + + let recipientsModeSection = recipientsModeSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.ChatbotSetup_RecipientsSectionHeader, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: [ + AnyComponentWithIdentity(id: "allExcept", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.BusinessMessageSetup_RecipientsOptionAllExcept, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + image: recentSessionCheckIcon, + tintColor: !hasAccessToAllChatsByDefault ? .clear : theme.list.itemAccentColor, + contentMode: .center + ))), false), + accessory: nil, + action: { [weak state] _ in + if !hasAccessToAllChatsByDefault { + state?.setConnectedBotAccessMode(excludeByDefault: true) + } + } + ))), + AnyComponentWithIdentity(id: "onlySelected", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.BusinessMessageSetup_RecipientsOptionOnly, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image( + image: recentSessionCheckIcon, + tintColor: hasAccessToAllChatsByDefault ? .clear : theme.list.itemAccentColor, + contentMode: .center + ))), false), + accessory: nil, + action: { [weak state] _ in + if hasAccessToAllChatsByDefault { + state?.setConnectedBotAccessMode(excludeByDefault: false) + } + } + ))) + ] + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(recipientsModeSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + recipientsModeSection.size.height / 2.0)) + ) + contentHeight += recipientsModeSection.size.height + contentHeight += 32.0 + + let recipientsSummarySection = recipientsSummarySection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .markdown( + text: hasAccessToAllChatsByDefault ? strings.ChatbotSetup_Recipients_ExcludedSectionFooter : strings.ChatbotSetup_Recipients_IncludedSectionFooter, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.itemAccentColor), + linkAttribute: { _ in + return nil + } + ) + ), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: "summary", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: hasAccessToAllChatsByDefault ? strings.ChatbotSetup_RecipientSummary_ExcludedChatsItem : strings.ChatbotSetup_RecipientSummary_IncludedChatsItem, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: nil, + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity( + id: "value", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: excludedSectionValue, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + )), + accessory: .arrow, + action: { [weak state] _ in + state?.openConnectedBotRecipients(isExclude: false) + } + ))) + ] + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(recipientsSummarySection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + recipientsSummarySection.size.height / 2.0)) + ) + contentHeight += recipientsSummarySection.size.height + contentHeight += 32.0 + + if !hasAccessToAllChatsByDefault { + let recipientsExcludedSection = recipientsExcludedSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .markdown( + text: strings.ChatbotSetup_Recipients_ExcludedSectionFooter, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.itemAccentColor), + linkAttribute: { _ in + return nil + } + ) + ), + maximumNumberOfLines: 0 + )), + items: [ + AnyComponentWithIdentity(id: "excluded", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.ChatbotSetup_RecipientSummary_ExcludedChatsItem, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: nil, + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity( + id: "value", + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: excludedUsersValue, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )) + )), + accessory: .arrow, + action: { [weak state] _ in + state?.openConnectedBotRecipients(isExclude: true) + } + ))) + ] + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(recipientsExcludedSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + recipientsExcludedSection.size.height / 2.0)) + ) + contentHeight += recipientsExcludedSection.size.height + contentHeight += 32.0 + } + } + if state.allowSecretChats != nil || state.allowIncomingCalls != nil { contentHeight += 38.0 @@ -523,31 +1029,9 @@ private final class RecentSessionSheetContent: CombinedComponent { } contentHeight += 32.0 - if let buttonString { + if buttonString != nil { 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.itemDestructiveColor, - foreground: .white, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) - ), - content: AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) - ), - action: { [weak state] in - state?.terminate() - } - ), - availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), - transition: .immediate - ) - context.add(button - .position(CGPoint(x: context.availableSize.width / 2.0 , y: contentHeight + button.size.height / 2.0)) - ) - contentHeight += button.size.height + contentHeight += 52.0 contentHeight += buttonInsets.bottom } @@ -578,57 +1062,103 @@ private final class RecentSessionSheetComponent: CombinedComponent { } static var body: Body { - let sheet = Child(SheetComponent.self) + let sheet = Child(ResizableSheetComponent.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 buttonTitle: String? + switch context.component.subject { + case let .session(session): + buttonTitle = !session.isCurrent ? environment.strings.AuthSessions_View_TerminateSession : nil + case .website: + buttonTitle = environment.strings.AuthSessions_View_Logout + case .connectedBot: + buttonTitle = environment.strings.AuthSessions_View_TerminateSession + } + let sheet = sheet.update( - component: SheetComponent( + component: ResizableSheetComponent( content: AnyComponent(RecentSessionSheetContent( context: context.component.context, subject: context.component.subject, cancel: { animate in - if animate { - animateOut.invoke(Action { _ in - if let controller = controller() { - controller.dismiss(completion: nil) - } - }) - } else if let controller = controller() { - controller.dismiss(animated: false, completion: nil) - } + dismiss(animate) } )), - style: .glass, + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: environment.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + hasTopEdgeEffect: false, + bottomItem: buttonTitle.flatMap { buttonTitle in + AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: environment.theme.list.itemDestructiveColor, + foreground: .white, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)) + ) + ) + ), + action: { + (controller() as? RecentSessionScreen)?.terminate() + } + ) + ) + }, backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), - followContentSizeChanges: true, - clipsContent: true, animateOut: animateOut ), environment: { environment - SheetComponentEnvironment( + ResizableSheetComponentEnvironment( + theme: environment.theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + inputHeight: environment.inputHeight, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, - hasInputHeight: !environment.inputHeight.isZero, + screenSize: context.availableSize, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { 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) - } - } + dismiss(animated) } ) }, @@ -649,9 +1179,11 @@ public class RecentSessionScreen: ViewControllerComponentContainer { public enum Subject { case session(RecentAccountSession) case website(WebAuthorization, EnginePeer?) + case connectedBot(TelegramAccountConnectedBot) } private let context: AccountContext + private let subject: Subject fileprivate let updateAcceptSecretChats: (Bool) -> Void fileprivate let updateAcceptIncomingCalls: (Bool) -> Void fileprivate let remove: (@escaping () -> Void) -> Void @@ -664,6 +1196,7 @@ public class RecentSessionScreen: ViewControllerComponentContainer { remove: @escaping (@escaping () -> Void) -> Void ) { self.context = context + self.subject = subject self.updateAcceptSecretChats = updateAcceptSecretChats self.updateAcceptIncomingCalls = updateAcceptIncomingCalls self.remove = remove @@ -692,8 +1225,22 @@ public class RecentSessionScreen: ViewControllerComponentContainer { self.view.disablesInteractiveModalDismiss = true } + fileprivate func terminate() { + switch self.subject { + case .session, .website: + self.remove({ [weak self] in + self?.dismissAnimated() + }) + case .connectedBot: + let _ = (self.context.engine.accountData.setAccountConnectedBot(bot: nil) + |> deliverOnMainQueue).startStandalone(completed: { [weak self] in + self?.dismissAnimated() + }) + } + } + public func dismissAnimated() { - if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { view.dismissAnimated() } } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 6395e90a2f..13a05e43ea 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -608,7 +608,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri var rightNavigationButton: ItemListNavigationButton? if !peers.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { state in var state = state state.editing = false diff --git a/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift b/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift index 116728780b..1727e136ba 100644 --- a/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift +++ b/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift @@ -15,6 +15,7 @@ import PresentationDataUtils import TelegramNotices import NotificationSoundSelectionUI import TelegramStringFormatting +import ContextUI private final class ReactionNotificationSettingsControllerArguments { let context: AccountContext @@ -270,12 +271,26 @@ private struct ReactionNotificationSettingsState: Equatable { } } +private final class ReactionNotificationSettingsContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0)) + } +} + public func reactionNotificationSettingsController( context: AccountContext, focusOnItemTag: ReactionNotificationSettingsEntryTag? = nil ) -> ViewController { - var presentControllerImpl: ((ViewController, Any?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? + var findCategoryReferenceNode: ((ReactionNotificationSettingsEntryTag) -> ItemListSwitchItemNode?)? + var currentReactionSettings = PeerReactionNotificationSettings.default let stateValue = Atomic(value: ReactionNotificationSettingsState()) let statePromise: ValuePromise = ValuePromise(ignoreRepeated: true) @@ -290,49 +305,48 @@ public func reactionNotificationSettingsController( let openCategory: (Bool) -> Void = { isMessages in let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - let text: String - if isMessages { - text = presentationData.strings.Notifications_Reactions_SheetTitleMessages - } else { - text = presentationData.strings.Notifications_Reactions_SheetTitleStories + + let currentValue: PeerReactionNotificationSettings.Sources = isMessages ? currentReactionSettings.messages : currentReactionSettings.stories + let options: [(PeerReactionNotificationSettings.Sources, String)] = [ + (.everyone, presentationData.strings.Notifications_Reactions_SheetValueEveryone), + (.contacts, presentationData.strings.Notifications_Reactions_SheetValueContacts) + ] + let items: [ContextMenuItem] = options.map { value, title in + return .action(ContextMenuActionItem(text: title, icon: { theme in + if currentValue == value { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) + let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + if isMessages { + settings.reactionSettings.messages = value + } else { + settings.reactionSettings.stories = value + } + return settings + }).start() + })) } - - let actionSheet = ActionSheetController(presentationData: presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: text), - ActionSheetButtonItem(title: presentationData.strings.Notifications_Reactions_SheetValueEveryone, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - if isMessages { - settings.reactionSettings.messages = .everyone - } else { - settings.reactionSettings.stories = .everyone - } - return settings - }).start() - }), - ActionSheetButtonItem(title: presentationData.strings.Notifications_Reactions_SheetValueContacts, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - if isMessages { - settings.reactionSettings.messages = .contacts - } else { - settings.reactionSettings.stories = .contacts - } - return settings - }).start() - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, nil) + + let sourceTag: ReactionNotificationSettingsEntryTag = isMessages ? .messages : .stories + guard let sourceNode = findCategoryReferenceNode?(sourceTag) else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(ReactionNotificationSettingsContextReferenceContentSource(sourceView: sourceNode.contextSourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) } let arguments = ReactionNotificationSettingsControllerArguments( @@ -399,6 +413,7 @@ public func reactionNotificationSettingsController( } else { viewSettings = GlobalNotificationSettingsSet.defaultSettings } + currentReactionSettings = viewSettings.reactionSettings let entries = reactionNotificationSettingsEntries( globalSettings: viewSettings, @@ -422,12 +437,15 @@ public func reactionNotificationSettingsController( } let controller = ItemListController(context: context, state: signal) - presentControllerImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) } pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } + findCategoryReferenceNode = { [weak controller] tag in + return controller?.itemNode(forTag: tag) as? ItemListSwitchItemNode + } if let focusOnItemTag { var didFocusOnItem = false diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift index 49c5dee31d..63a4d5a893 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift @@ -231,7 +231,7 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode { var revealOptions: [ItemListRevealOption] = [] if item.isFaq { } else { - revealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)) + revealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.theme.list.itemSecondaryTextColor)) } strongSelf.setRevealOptions((left: [], right: revealOptions)) } diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index edf9021487..ef18cdaa6e 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -3133,6 +3133,18 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac } ) ) + items.append( + SettingsSearchableItem( + id: "privacy/open-links", + title: strings.ChatSettings_OpenLinksIn, + alternate: synonyms(strings.SettingsSearch_Synonyms_ChatSettings_OpenLinksIn), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + present(.push, webBrowserSettingsController(context: context)) + } + ) + ) items.append( SettingsSearchableItem( id: "privacy/data-settings/delete-synced", @@ -3670,16 +3682,6 @@ private func dataSearchableItems(context: AccountContext) -> [SettingsSearchable presentDataSettings(context, present, .sensitiveContent) } ), - SettingsSearchableItem( - id: "data/open-links", - title: strings.ChatSettings_OpenLinksIn, - alternate: synonyms(strings.SettingsSearch_Synonyms_ChatSettings_OpenLinksIn), - icon: icon, - breadcrumbs: [strings.Settings_ChatSettings], - present: { context, _, present in - present(.push, webBrowserSettingsController(context: context)) - } - ), SettingsSearchableItem( id: "data/share-sheet", title: strings.ChatSettings_IntentsSettings, diff --git a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift index db053a13a4..d0d017c115 100644 --- a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift @@ -457,7 +457,7 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv var toolbarItem: ItemListToolbarItem? if let packs = packs, packs.count != 0 { if state.editing { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { $0.withUpdatedEditing(false) } diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index 6ba3bb2f34..b30559b2a7 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -13,6 +13,7 @@ import StickerPackPreviewUI import ItemListStickerPackItem import ItemListPeerActionItem import UndoUI +import ContextUI import WebPBinding import ReactionImageComponent @@ -507,6 +508,18 @@ private struct InstalledStickerPacksControllerState: Equatable { } } +private final class InstalledStickerPacksSuggestOptionsContextReferenceContentSource: ContextReferenceContentSource { + private let sourceView: UIView + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0)) + } +} + private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> EngineItemCollectionId.Namespace { switch mode { case .general, .modal: @@ -662,9 +675,12 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var presentInGlobalOverlayImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var navigateToChatControllerImpl: ((EnginePeer.Id) -> Void)? var dismissImpl: (() -> Void)? + var findSuggestOptionsReferenceNode: (() -> ItemListDisclosureItemNode?)? + var currentEmojiStickerSuggestionMode = StickerSettings.defaultSettings.emojiStickerSuggestionMode let actionsDisposable = DisposableSet() @@ -801,30 +817,41 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta updatedPacks(packs) })) }, openSuggestOptions: { - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } let options: [(EmojiStickerSuggestionMode, String)] = [ (.all, presentationData.strings.Stickers_SuggestAll), (.installed, presentationData.strings.Stickers_SuggestAdded), (.none, presentationData.strings.Stickers_SuggestNone) ] - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: presentationData.strings.Stickers_SuggestStickers)) + var items: [ContextMenuItem] = [] for (option, title) in options { - items.append(ActionSheetButtonItem(title: title, color: .accent, action: { - dismissAction() + items.append(.action(ContextMenuActionItem(text: title, icon: { theme in + if currentEmojiStickerSuggestionMode == option { + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + } else { + return UIImage() + } + }, action: { _, f in + f(.default) let _ = updateStickerSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedEmojiStickerSuggestionMode(option) }).start() - })) + }))) } - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + + guard let sourceNode = findSuggestOptionsReferenceNode?() else { + return + } + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(InstalledStickerPacksSuggestOptionsContextReferenceContentSource(sourceView: sourceNode.labelNode.view)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + sourceNode.updateHasContextMenu(hasContextMenu: true) + contextController.dismissed = { [weak sourceNode] in + sourceNode?.updateHasContextMenu(hasContextMenu: false) + } + presentInGlobalOverlayImpl?(contextController) }, toggleSuggestAnimatedEmoji: { value in let _ = updateStickerSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedSuggestAnimatedEmoji(value) @@ -938,6 +965,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) { stickerSettings = value } + currentEmojiStickerSuggestionMode = stickerSettings.emojiStickerSuggestionMode var packCount: Int? = nil var stickerPacks: [EngineRawItemCollectionInfoEntry] = [] @@ -954,7 +982,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta if case .modal = mode { rightNavigationButton = nil } else { - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { $0.withUpdatedEditing(false).withUpdatedSelectedPackIds(nil) } @@ -1213,6 +1241,9 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta controller.present(c, in: .window(.root), with: p) } } + presentInGlobalOverlayImpl = { [weak controller] c in + controller?.presentInGlobalOverlay(c, with: nil) + } presentStickerPackController = { [weak controller] info in let _ = (stickerPacks.get() |> take(1) @@ -1298,6 +1329,9 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta dismissImpl = { [weak controller] in controller?.dismiss() } + findSuggestOptionsReferenceNode = { [weak controller] in + return controller?.itemNode(forTag: InstalledStickerPacksEntryTag.suggestOptions) as? ItemListDisclosureItemNode + } if let focusOnItemTag { var didFocusOnItem = false diff --git a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift index 6243b349b6..6855051a2a 100644 --- a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift +++ b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceControllerNode.swift @@ -199,6 +199,12 @@ final class TermsOfServiceControllerNode: ViewControllerTracingNode { deinit { } + override func didLoad() { + super.didLoad() + + self.scrollNode.view.scrollsToTop = false + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 6c86563d64..540b821cf4 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -188,6 +188,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.pageControlNode.setPage(0.0) } @@ -239,6 +240,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in + }, performBotConnectionReviewAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, openStarsTopup: { _ in diff --git a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift index ad9cc43121..4b0d52dd75 100644 --- a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift +++ b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift @@ -25,7 +25,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec if let image = cachedBorderImages[key] { return image } else { - let image = generateImage(CGSize(width: 18.0, height: 18.0), rotatedContext: { size, context in + let image = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -51,7 +51,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec context.setLineWidth(lineWidth) context.strokeEllipse(in: bounds.insetBy(dx: 1.0 + lineWidth / 2.0, dy: 1.0 + lineWidth / 2.0)) } - })?.stretchableImage(withLeftCapWidth: 9, topCapHeight: 9) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 16) cachedBorderImages[key] = image return image } @@ -80,7 +80,7 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode { self.imageNode = TransformImageNode() self.imageNode.isLayerBacked = true - self.imageNode.cornerRadius = 8.0 + self.imageNode.cornerRadius = 16.0 self.imageNode.clipsToBounds = true self.overlayNode = ASImageNode() @@ -153,11 +153,6 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode { } } -// override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { -// let emojiFrame = CGRect(origin: CGPoint(x: 33.0, y: 79.0), size: CGSize(width: 24.0, height: 24.0)) -// self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + emojiFrame.minX, y: rect.minY + emojiFrame.minY), size: emojiFrame.size), within: containerSize) -// } - func setup(item: ThemeCarouselThemeIconItem, size: CGSize) { let currentItem = self.item let currentSize = self.size @@ -417,6 +412,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { let insets: UIEdgeInsets let separatorHeight = UIScreenPixel + let padding: CGFloat = 12.0 let minSpacing: CGFloat = 6.0 let referenceImageSize: CGSize @@ -428,18 +424,13 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { } let totalWidth = params.width - params.leftInset - params.rightInset let imageCount = Int((totalWidth - minSpacing) / (referenceImageSize.width + minSpacing)) - var itemSize = referenceImageSize.aspectFilled(CGSize(width: floorToScreenPixels((totalWidth - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) + var itemSize = referenceImageSize.aspectFilled(CGSize(width: floorToScreenPixels((totalWidth - padding * 2.0 - CGFloat(imageCount - 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) itemSize.height = referenceImageSize.height - let itemSpacing = floorToScreenPixels((totalWidth - CGFloat(imageCount) * itemSize.width) / CGFloat(imageCount + 1)) - - var spacingOffset: CGFloat = 0.0 - if totalWidth - CGFloat(imageCount) * itemSize.width - CGFloat(imageCount + 1) * itemSpacing == 1.0 { - spacingOffset = UIScreenPixel - } - + let itemSpacing = floorToScreenPixels((totalWidth - padding * 2.0 - CGFloat(imageCount) * itemSize.width) / CGFloat(imageCount - 1)) + let rows = ceil(CGFloat(item.themes.count) / CGFloat(imageCount)) - contentSize = CGSize(width: params.width, height: minSpacing + rows * (itemSize.height + itemSpacing)) + contentSize = CGSize(width: params.width, height: padding * 2.0 + rows * itemSize.height + (rows - 1.0) * itemSpacing) insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -450,6 +441,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { strongSelf.item = item strongSelf.layoutParams = params + strongSelf.scrollNode.view.scrollsToTop = false strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor @@ -492,7 +484,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height) - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -525,7 +517,7 @@ class ThemeGridThemeItemNode: ListViewItemNode, ItemListItemNode { let col = CGFloat(index % imageCount) let row = floor(CGFloat(index) / CGFloat(imageCount)) - let itemFrame = CGRect(origin: CGPoint(x: params.leftInset + spacingOffset + itemSpacing + (itemSize.width + itemSpacing) * col, y: minSpacing + (itemSize.height + itemSpacing) * row), size: itemSize) + let itemFrame = CGRect(origin: CGPoint(x: params.leftInset + padding + (itemSize.width + itemSpacing) * col, y: padding + (itemSize.height + itemSpacing) * row), size: itemSize) itemNode.frame = itemFrame index += 1 diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index 32112be4b5..697a1ca703 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -496,7 +496,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), previewThemePromise.get(), settingsPromise.get()) |> map { presentationData, state, previewTheme, currentSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) @@ -516,7 +516,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll isComplete = !state.title.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty && !state.slug.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty } - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: isComplete, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: isComplete, action: { if state.title.count > 128 { errorImpl?(.title) return diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift index 799c42fb69..1f206a2bfb 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/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index 80bcb63874..077a07ec0c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift @@ -59,7 +59,9 @@ public final class ThemePreviewController: ViewController { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationTheme.set(.single(previewTheme)) - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.previewTheme, presentationStrings: self.presentationData.strings)) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.previewTheme, presentationStrings: self.presentationData.strings, style: .glass)) + + self._hasGlassStyle = true self.blocksBackgroundWhenInOverlay = true self.acceptsFocusWhenInOverlay = true @@ -140,13 +142,13 @@ public final class ThemePreviewController: ViewController { let titleView = CounterControllerTitleView(theme: self.previewTheme) titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? " " : "") self.navigationItem.titleView = titleView - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.statusBar.statusBarStyle = self.previewTheme.rootController.statusBarStyle.style self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) if !isPreview { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: self.previewTheme.rootController.navigationBar.accentTextColor), style: .plain, target: self, action: #selector(self.actionPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Navigation/Share"), color: self.previewTheme.chat.inputPanel.panelControlColor), style: .plain, target: self, action: #selector(self.actionPressed)) } self.disposable = (combineLatest(self.theme.get(), self.presentationTheme.get()) @@ -155,7 +157,7 @@ public final class ThemePreviewController: ViewController { let titleView = CounterControllerTitleView(theme: strongSelf.previewTheme) titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? strongSelf.presentationData.strings.Theme_UsersCount(max(1, theme.installCount ?? 0)) : "") strongSelf.navigationItem.titleView = titleView - strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: presentationTheme, presentationStrings: strongSelf.presentationData.strings), transition: .immediate) + strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: presentationTheme, presentationStrings: strongSelf.presentationData.strings, style: .glass), transition: .immediate) } }) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index ff77efe3b5..bbad3bbf56 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -298,7 +298,8 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { } strongSelf.remoteChatBackgroundNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), custom: patternArguments))() - strongSelf.toolbarNode.dark = useDarkButton + strongSelf.toolbarNode.dark = false + let _ = useDarkButton } }) } @@ -318,6 +319,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.pageControlNode.setPage(0.0) } @@ -373,6 +375,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in + }, performBotConnectionReviewAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, openStarsTopup: { _ in diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift index fad4ad8e12..17c4abb873 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAccentColorItem.swift @@ -842,7 +842,7 @@ class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode { } strongSelf.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height) - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index 8140064aff..3db095dc8c 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -179,7 +179,7 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { self.iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - iconSize.width) / 2.0), y: 13.0), size: iconSize) self.overlayNode.frame = self.iconNode.frame - let textSize = self.textNode.updateLayout(bounds.size) + let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width + 8.0, height: bounds.size.height)) let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: 81.0), size: textSize) self.textNode.frame = textFrame diff --git a/submodules/SettingsUI/Sources/UsernameSetupController.swift b/submodules/SettingsUI/Sources/UsernameSetupController.swift index 97434becf5..a605c29dbd 100644 --- a/submodules/SettingsUI/Sources/UsernameSetupController.swift +++ b/submodules/SettingsUI/Sources/UsernameSetupController.swift @@ -564,7 +564,7 @@ public func usernameSetupController(context: AccountContext, mode: UsernameSetup } } - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in if state.editingPublicLinkText != peer.addressName { @@ -597,7 +597,7 @@ public func usernameSetupController(context: AccountContext, mode: UsernameSetup }) } - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 492089f2e9..7566da5364 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -456,6 +456,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() if self.fromForeignApp { diff --git a/submodules/StatisticsUI/Sources/StatsGraphItem.swift b/submodules/StatisticsUI/Sources/StatsGraphItem.swift index ce10ccaeea..0209f6637b 100644 --- a/submodules/StatisticsUI/Sources/StatsGraphItem.swift +++ b/submodules/StatisticsUI/Sources/StatsGraphItem.swift @@ -320,7 +320,7 @@ public final class StatsGraphItemNode: ListViewItemNode { strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) - strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - 16.0) / 2.0), y: floor((layout.size.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0)) + strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: floor((strongSelf.chartContainerNode.frame.width - 16.0) / 2.0), y: floor((strongSelf.chartContainerNode.frame.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0)) strongSelf.errorTextNode.frame = CGRect( origin: CGPoint( x: floorToScreenPixels((strongSelf.chartContainerNode.bounds.width - errorTextLayout.size.width) / 2.0), diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index dc2f656c36..5fbf1ed03e 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -22,9 +22,9 @@ final class StickerPackPreviewInteraction { let removeStickerPack: (StickerPackCollectionInfo) -> Void let emojiSelected: (String, ChatTextInputTextCustomEmojiAttribute) -> Void let emojiLongPressed: (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void - let addPressed: () -> Void + let addPressed: (UIView) -> Void - init(playAnimatedStickers: Bool, addStickerPack: @escaping (StickerPackCollectionInfo, [StickerPackItem]) -> Void, removeStickerPack: @escaping (StickerPackCollectionInfo) -> Void, emojiSelected: @escaping (String, ChatTextInputTextCustomEmojiAttribute) -> Void, emojiLongPressed: @escaping (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void, addPressed: @escaping () -> Void) { + init(playAnimatedStickers: Bool, addStickerPack: @escaping (StickerPackCollectionInfo, [StickerPackItem]) -> Void, removeStickerPack: @escaping (StickerPackCollectionInfo) -> Void, emojiSelected: @escaping (String, ChatTextInputTextCustomEmojiAttribute) -> Void, emojiLongPressed: @escaping (String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void, addPressed: @escaping (UIView) -> Void) { self.playAnimatedStickers = playAnimatedStickers self.addStickerPack = addStickerPack self.removeStickerPack = removeStickerPack @@ -128,7 +128,6 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.containerNode = ASDisplayNode() self.imageNode = TransformImageNode() - self.imageNode.isLayerBacked = !smartInvertColorsEnabled() self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode.isUserInteractionEnabled = false @@ -194,7 +193,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { } @objc private func handleAddTap() { - self.interaction?.addPressed() + self.interaction?.addPressed(self.imageNode.view) } private var setupTimestamp: Double? diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 4a968b8c6f..8aeb09cc88 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -269,7 +269,7 @@ private final class StickerPackContainer: ASDisplayNode { var removeStickerPackImpl: ((StickerPackCollectionInfo) -> Void)? var emojiSelectedImpl: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)? var emojiLongPressedImpl: ((String, ChatTextInputTextCustomEmojiAttribute, ASDisplayNode, CGRect) -> Void)? - var addPressedImpl: (() -> Void)? + var addPressedImpl: ((UIView) -> Void)? self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true, addStickerPack: { info, items in addStickerPackImpl?(info, items) }, removeStickerPack: { info in @@ -278,8 +278,8 @@ private final class StickerPackContainer: ASDisplayNode { emojiSelectedImpl?(text, attribute) }, emojiLongPressed: { text, attribute, node, frame in emojiLongPressedImpl?(text, attribute, node, frame) - }, addPressed: { - addPressedImpl?() + }, addPressed: { sourceView in + addPressedImpl?(sourceView) }) super.init() @@ -326,33 +326,39 @@ private final class StickerPackContainer: ASDisplayNode { return targetOffset } - let insets = strongSelf.gridNode.scrollView.contentInset + guard let (layout, _, titleAreaInset, gridInsets) = strongSelf.validLayout else { + return targetOffset + } + + let collapsedOffset = -gridInsets.top + let expandedOffset = strongSelf.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) var modalProgress: CGFloat = 0.0 var updatedOffset = targetOffset var resetOffset = false - if targetOffset.y < 0.0 && targetOffset.y >= -insets.top { - if contentOffset.y > 0.0 { - updatedOffset = CGPoint(x: 0.0, y: 0.0) + if targetOffset.y < expandedOffset && targetOffset.y >= collapsedOffset { + if contentOffset.y > expandedOffset { + updatedOffset = CGPoint(x: 0.0, y: expandedOffset) modalProgress = 1.0 } else { - if targetOffset.y > -insets.top / 2.0 || velocity.y <= -100.0 { + let middleOffset = collapsedOffset + (expandedOffset - collapsedOffset) / 2.0 + if targetOffset.y > middleOffset || velocity.y <= -100.0 { modalProgress = 1.0 resetOffset = true } else { modalProgress = 0.0 - if contentOffset.y > -insets.top { + if contentOffset.y > collapsedOffset { resetOffset = true } } } - } else if targetOffset.y >= 0.0 { + } else if targetOffset.y >= expandedOffset { modalProgress = 1.0 } if abs(strongSelf.modalProgress - modalProgress) > CGFloat.ulpOfOne { - if contentOffset.y > 0.0 && targetOffset.y > 0.0 { + if contentOffset.y > expandedOffset && targetOffset.y > expandedOffset { } else { resetOffset = true } @@ -364,10 +370,10 @@ private final class StickerPackContainer: ASDisplayNode { let offset: CGPoint let isVelocityAligned: Bool if modalProgress.isZero { - offset = CGPoint(x: 0.0, y: -insets.top) + offset = CGPoint(x: 0.0, y: collapsedOffset) isVelocityAligned = velocity.y < 0.0 } else { - offset = CGPoint(x: 0.0, y: 0.0) + offset = CGPoint(x: 0.0, y: expandedOffset) isVelocityAligned = velocity.y > 0.0 } @@ -459,8 +465,8 @@ private final class StickerPackContainer: ASDisplayNode { longPressEmoji?(text, attribute, node, frame) } - addPressedImpl = { [weak self] in - self?.presentAddStickerOptions() + addPressedImpl = { [weak self] sourceView in + self?.presentAddStickerOptions(sourceView: sourceView) } } @@ -1209,33 +1215,44 @@ private final class StickerPackContainer: ASDisplayNode { } private let stickerPickerInputData = Promise() - private func presentAddStickerOptions() { - let actionSheet = ActionSheetController(presentationData: self.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetButtonItem(title: self.presentationData.strings.StickerPack_CreateNew, color: .accent, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() - + private func presentAddStickerOptions(sourceView: UIView) { + guard let controller = self.controller else { + return + } + + let presentationData = self.presentationData + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_CreateNew, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + guard let self, let controller = self.controller else { return } self.presentCreateSticker() controller.controllerNode.dismiss() - })) - items.append(ActionSheetButtonItem(title: self.presentationData.strings.StickerPack_AddExisting, color: .accent, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() + }))) + items.append(.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_AddExisting, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddSticker"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) guard let self, let controller = self.controller else { return } self.presentAddExistingSticker() controller.controllerNode.dismiss() - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - self.presentInGlobalOverlay(actionSheet, nil) + }))) + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: nil + ) + self.presentInGlobalOverlay(contextController, nil) let stickerItems = EmojiPagerContentComponent.stickerInputData( context: self.context, @@ -1598,8 +1615,8 @@ private final class StickerPackContainer: ASDisplayNode { self.onReady() if !contents.isEmpty && self.currentStickerPacks.isEmpty { - if let _ = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { - scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: false) + if let (layout, _, titleAreaInset, gridInsets) = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { + scrollToItem = GridNodeScrollToItem(index: 0, position: .top(self.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets)), transition: .immediate, directionHint: .up, adjustForSection: false) } } @@ -1671,8 +1688,8 @@ private final class StickerPackContainer: ASDisplayNode { let info = info._parse() if !items.isEmpty && self.currentStickerPack == nil { - if let _ = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { - scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: false) + if let (layout, _, titleAreaInset, gridInsets) = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne { + scrollToItem = GridNodeScrollToItem(index: 0, position: .top(self.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets)), transition: .immediate, directionHint: .up, adjustForSection: false) } } @@ -1875,12 +1892,37 @@ private final class StickerPackContainer: ASDisplayNode { return min(self.backgroundNode.frame.minY, gridFrame.minY + gridInsets.top - titleAreaInset) } + private func expandedContentOffset(layout: ContainerViewLayout, titleAreaInset: CGFloat, gridInsets: UIEdgeInsets) -> CGFloat { + if case .regular = layout.metrics.widthClass { + return 0.0 + } else { + return max(-gridInsets.top, -titleAreaInset) + } + } + + private func expandProgress(contentOffset: CGFloat, layout: ContainerViewLayout, titleAreaInset: CGFloat, gridInsets: UIEdgeInsets) -> CGFloat { + let collapsedOffset = -gridInsets.top + let expandedOffset = self.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) + let distance = expandedOffset - collapsedOffset + if distance <= CGFloat.ulpOfOne { + return 1.0 + } + return max(0.0, min(1.0, (contentOffset - collapsedOffset) / distance)) + } + + private func contentOffsetForExpandProgress(_ expandProgress: CGFloat, layout: ContainerViewLayout, titleAreaInset: CGFloat, gridInsets: UIEdgeInsets) -> CGFloat { + let collapsedOffset = -gridInsets.top + let expandedOffset = self.expandedContentOffset(layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) + let progress = max(0.0, min(1.0, expandProgress)) + return collapsedOffset + (expandedOffset - collapsedOffset) * progress + } + func syncExpandProgress(expandScrollProgress: CGFloat, expandProgress: CGFloat, modalProgress: CGFloat, transition: ContainedViewLayoutTransition) { - guard let (_, _, _, gridInsets) = self.validLayout else { + guard let (layout, _, titleAreaInset, gridInsets) = self.validLayout else { return } - let contentOffset = (1.0 - expandScrollProgress) * (-gridInsets.top) + let contentOffset = self.contentOffsetForExpandProgress(expandScrollProgress, layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) if case let .animated(duration, _) = transition { self.gridNode.autoscroll(toOffset: CGPoint(x: 0.0, y: contentOffset), duration: duration) } else { @@ -2152,11 +2194,11 @@ private final class StickerPackContainer: ASDisplayNode { let topEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: 90.0)) transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame) - self.topEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) + self.topEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.9, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: actionAreaHeight - 90.0), size: CGSize(width: layout.size.width, height: 90.0)) transition.updateFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame) - self.bottomEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) + self.bottomEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.9, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) if firstTime { while !self.enqueuedTransactions.isEmpty { @@ -2178,10 +2220,8 @@ private final class StickerPackContainer: ASDisplayNode { let minBackgroundY = gridFrame.minY let unclippedBackgroundY = gridFrame.minY - presentationLayout.contentOffset.y - titleAreaInset - let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top - let expandHeight: CGFloat = 100.0 - let expandProgress = max(0.0, min(1.0, offsetFromInitialPosition / expandHeight)) - let expandScrollProgress = 1.0 - max(0.0, min(1.0, presentationLayout.contentOffset.y / (-gridInsets.top))) + let expandProgress = self.expandProgress(contentOffset: presentationLayout.contentOffset.y, layout: layout, titleAreaInset: titleAreaInset, gridInsets: gridInsets) + let expandScrollProgress = expandProgress let modalProgress = max(0.0, min(1.0, expandScrollProgress)) let expandProgressTransition = transition diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index e76f43d4a4..1ffa22114d 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -352,6 +352,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.tapRecognizer = tapRecognizer diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 58b4bd3d1a..f6df75ee72 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -313,34 +313,34 @@ public final class CallController: ViewController { } } - self.controllerNode.callEnded = { [weak self] didPresentRating in - if let strongSelf = self, !didPresentRating { - let _ = (combineLatest(strongSelf.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings]), ApplicationSpecificNotice.getCallsTabTip(accountManager: strongSelf.sharedContext.accountManager)) - |> map { sharedData, callsTabTip -> Int32 in - var value = false - if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings]?.get(CallListSettings.self) { - value = settings.showTab - } - if value { - return 3 - } else { - return callsTabTip - } - } |> deliverOnMainQueue).start(next: { [weak self] callsTabTip in - if let strongSelf = self { - if callsTabTip == 2 { - Queue.mainQueue().after(1.0) { - let controller = callSuggestTabController(sharedContext: strongSelf.sharedContext) - strongSelf.present(controller, in: .window(.root)) - } - } - if callsTabTip < 3 { - let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.sharedContext.accountManager).start() - } - } - }) - } - } +// self.controllerNode.callEnded = { [weak self] didPresentRating in +// if let strongSelf = self, !didPresentRating { +// let _ = (combineLatest(strongSelf.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings]), ApplicationSpecificNotice.getCallsTabTip(accountManager: strongSelf.sharedContext.accountManager)) +// |> map { sharedData, callsTabTip -> Int32 in +// var value = false +// if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings]?.get(CallListSettings.self) { +// value = settings.showTab +// } +// if value { +// return 3 +// } else { +// return callsTabTip +// } +// } |> deliverOnMainQueue).start(next: { [weak self] callsTabTip in +// if let strongSelf = self { +// if callsTabTip == 2 { +// Queue.mainQueue().after(1.0) { +// let controller = callSuggestTabController(sharedContext: strongSelf.sharedContext) +// strongSelf.present(controller, in: .window(.root)) +// } +// } +// if callsTabTip < 3 { +// let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.sharedContext.accountManager).start() +// } +// } +// }) +// } +// } self.controllerNode.willBeDismissedInteractively = { [weak self] in guard let self else { diff --git a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift index fbd9141072..7b491b7ffc 100644 --- a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift +++ b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift @@ -293,7 +293,7 @@ public func callFeedbackController(sharedContext: SharedAccountContext, account: let signal = combineLatest(sharedContext.presentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.CallFeedback_Send), style: .bold, enabled: true, action: { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift index f950e540f0..b0adada425 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift @@ -158,6 +158,7 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, A self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index a158d695d3..ec5b336544 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -241,6 +241,7 @@ public final class VoiceChatJoinScreen: ViewController { self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift index 8d61502466..0e5c02ff8e 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatRecordingSetupController.swift @@ -158,6 +158,7 @@ private class VoiceChatRecordingSetupControllerNode: ViewControllerTracingNode, self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift index a697ec8ff6..fd5c7fa427 100644 --- a/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift +++ b/submodules/TelegramCore/Sources/Account/AccountIntermediateState.swift @@ -124,7 +124,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]) @@ -146,6 +145,9 @@ enum AccountStateMutationOperation { case UpdateStoryStealthMode(data: Api.StoriesStealthMode) case UpdateStorySentReaction(peerId: PeerId, id: Int32, reaction: Api.Reaction) case UpdateNewAuthorization(isUnconfirmed: Bool, hash: Int64, date: Int32, device: String, location: String) + case UpdateNewBotConnection(confirmed: Bool, botId: PeerId, date: Int32?, device: String?, location: String?) + case UpdateWebBrowserSettings(openExternalBrowser: Bool) + case UpdateWebBrowserException(openExternalBrowser: Bool?, delete: Bool, exception: AccountWebBrowserException) case UpdateWallpaper(peerId: PeerId, wallpaper: TelegramWallpaper?) case UpdateStarsBalance(peerId: PeerId, currency: CurrencyAmount.Currency, balance: StarsAmount) case UpdateStarsRevenueStatus(peerId: PeerId, status: StarsRevenueStats.Balances) @@ -242,6 +244,7 @@ struct AccountMutableState { var storedMessagesByPeerIdAndTimestamp: [PeerId: Set] var displayAlerts: [(text: String, isDropAuth: Bool)] = [] var dismissBotWebViews: [Int64] = [] + var joinChatWebViewDecisions: [JoinChatWebViewDecision] = [] var insertedPeers: [PeerId: Peer] = [:] @@ -274,7 +277,7 @@ struct AccountMutableState { self.updatedOutgoingUniqueMessageIds = [:] } - init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, storedStories: [StoryId: UpdatesStoredStory], sentScheduledMessageIds: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], branchOperationIndex: Int) { + init(initialState: AccountInitialState, operations: [AccountStateMutationOperation], state: AuthorizedAccountState.State, peers: [PeerId: Peer], apiChats: [PeerId: Api.Chat], channelStates: [PeerId: AccountStateChannelState], peerChatInfos: [PeerId: PeerChatInfo], referencedReplyMessageIds: ReferencedReplyMessageIds, referencedGeneralMessageIds: Set, storedMessages: Set, storedStories: [StoryId: UpdatesStoredStory], sentScheduledMessageIds: Set, readInboxMaxIds: [PeerId: MessageId], storedMessagesByPeerIdAndTimestamp: [PeerId: Set], namespacesWithHolesFromPreviousState: [PeerId: [MessageId.Namespace: HoleFromPreviousState]], updatedOutgoingUniqueMessageIds: [Int64: Int32], displayAlerts: [(text: String, isDropAuth: Bool)], dismissBotWebViews: [Int64], joinChatWebViewDecisions: [JoinChatWebViewDecision], branchOperationIndex: Int) { self.initialState = initialState self.operations = operations self.state = state @@ -293,11 +296,12 @@ struct AccountMutableState { self.updatedOutgoingUniqueMessageIds = updatedOutgoingUniqueMessageIds self.displayAlerts = displayAlerts self.dismissBotWebViews = dismissBotWebViews + self.joinChatWebViewDecisions = joinChatWebViewDecisions self.branchOperationIndex = branchOperationIndex } func branch() -> AccountMutableState { - return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, storedStories: self.storedStories, sentScheduledMessageIds: self.sentScheduledMessageIds, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, branchOperationIndex: self.operations.count) + return AccountMutableState(initialState: self.initialState, operations: self.operations, state: self.state, peers: self.peers, apiChats: self.apiChats, channelStates: self.channelStates, peerChatInfos: self.peerChatInfos, referencedReplyMessageIds: self.referencedReplyMessageIds, referencedGeneralMessageIds: self.referencedGeneralMessageIds, storedMessages: self.storedMessages, storedStories: self.storedStories, sentScheduledMessageIds: self.sentScheduledMessageIds, readInboxMaxIds: self.readInboxMaxIds, storedMessagesByPeerIdAndTimestamp: self.storedMessagesByPeerIdAndTimestamp, namespacesWithHolesFromPreviousState: self.namespacesWithHolesFromPreviousState, updatedOutgoingUniqueMessageIds: self.updatedOutgoingUniqueMessageIds, displayAlerts: self.displayAlerts, dismissBotWebViews: self.dismissBotWebViews, joinChatWebViewDecisions: self.joinChatWebViewDecisions, branchOperationIndex: self.operations.count) } mutating func merge(_ other: AccountMutableState) { @@ -341,6 +345,7 @@ struct AccountMutableState { self.updatedOutgoingUniqueMessageIds.merge(other.updatedOutgoingUniqueMessageIds, uniquingKeysWith: { lhs, _ in lhs }) self.displayAlerts.append(contentsOf: other.displayAlerts) self.dismissBotWebViews.append(contentsOf: other.dismissBotWebViews) + self.joinChatWebViewDecisions.append(contentsOf: other.joinChatWebViewDecisions) self.resetForumTopicLists.merge(other.resetForumTopicLists, uniquingKeysWith: { lhs, _ in lhs }) } @@ -377,6 +382,10 @@ struct AccountMutableState { self.dismissBotWebViews.append(queryId) } + mutating func addJoinChatWebViewDecision(_ decision: JoinChatWebViewDecision) { + self.joinChatWebViewDecisions.append(decision) + } + mutating func deleteMessagesWithGlobalIds(_ globalIds: [Int32]) { self.addOperation(.DeleteMessagesWithGlobalIds(globalIds)) } @@ -569,11 +578,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)) } @@ -728,6 +733,18 @@ struct AccountMutableState { self.addOperation(.UpdateNewAuthorization(isUnconfirmed: isUnconfirmed, hash: hash, date: date, device: device, location: location)) } + mutating func updateNewBotConnection(confirmed: Bool, botId: PeerId, date: Int32?, device: String?, location: String?) { + self.addOperation(.UpdateNewBotConnection(confirmed: confirmed, botId: botId, date: date, device: device, location: location)) + } + + mutating func updateWebBrowserSettings(openExternalBrowser: Bool) { + self.addOperation(.UpdateWebBrowserSettings(openExternalBrowser: openExternalBrowser)) + } + + mutating func updateWebBrowserException(openExternalBrowser: Bool?, delete: Bool, exception: AccountWebBrowserException) { + self.addOperation(.UpdateWebBrowserException(openExternalBrowser: openExternalBrowser, delete: delete, exception: exception)) + } + mutating func updateStarsBalance(peerId: PeerId, currency: CurrencyAmount.Currency, balance: StarsAmount) { self.addOperation(.UpdateStarsBalance(peerId: peerId, currency: currency, balance: balance)) } @@ -762,7 +779,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, .UpdateNewBotConnection, .UpdateWebBrowserSettings, .UpdateWebBrowserException, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState, .UpdateEmojiGameInfo: break case let .AddMessages(messages, location): for message in messages { @@ -900,7 +917,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] @@ -931,10 +947,10 @@ 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] + let joinChatWebViewDecisions: [JoinChatWebViewDecision] let delayNotificatonsUntil: Int32? let updatedMaxMessageId: Int32? let updatedQts: Int32? @@ -954,10 +970,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.joinChatWebViewDecisions.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] = [], joinChatWebViewDecisions: [JoinChatWebViewDecision] = [], 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 @@ -969,10 +985,10 @@ struct AccountFinalStateEvents { self.updatedGroupCallParticipants = updatedGroupCallParticipants self.groupCallMessageUpdates = groupCallMessageUpdates self.storyUpdates = storyUpdates - self.updatedPeersNearby = updatedPeersNearby self.isContactUpdates = isContactUpdates self.displayAlerts = displayAlerts self.dismissBotWebViews = dismissBotWebViews + self.joinChatWebViewDecisions = joinChatWebViewDecisions self.delayNotificatonsUntil = delayNotificatonsUntil self.updatedMaxMessageId = updatedMaxMessageId self.updatedQts = updatedQts @@ -1005,10 +1021,10 @@ 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 + self.joinChatWebViewDecisions = state.state.state.joinChatWebViewDecisions self.delayNotificatonsUntil = state.delayNotificatonsUntil self.updatedMaxMessageId = state.state.state.updatedMaxMessageId self.updatedQts = state.state.state.updatedQts @@ -1080,6 +1096,7 @@ struct AccountFinalStateEvents { isContactUpdates: self.isContactUpdates + other.isContactUpdates, displayAlerts: self.displayAlerts + other.displayAlerts, dismissBotWebViews: self.dismissBotWebViews + other.dismissBotWebViews, + joinChatWebViewDecisions: self.joinChatWebViewDecisions + other.joinChatWebViewDecisions, delayNotificatonsUntil: delayNotificatonsUntil, updatedMaxMessageId: updatedMaxMessageId, updatedQts: updatedQts, diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index 624a4bbcf7..8bdb2d2f9c 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -135,7 +135,7 @@ extension TelegramUser { botFlags.insert(.isGuestChat) } if (flags2 & (1 << 20)) != 0 { - botFlags.insert(.botGuard) + botFlags.insert(.isGuardBot) } botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder) } diff --git a/submodules/TelegramCore/Sources/Settings/AccountWebBrowserSettings.swift b/submodules/TelegramCore/Sources/Settings/AccountWebBrowserSettings.swift new file mode 100644 index 0000000000..52a6303502 --- /dev/null +++ b/submodules/TelegramCore/Sources/Settings/AccountWebBrowserSettings.swift @@ -0,0 +1,237 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi + +public struct AccountWebBrowserException: Codable, Equatable { + public let domain: String + public let url: String + public let title: String + public let favicon: Int64? + + public init(domain: String, url: String, title: String, favicon: Int64?) { + self.domain = domain + self.url = url + self.title = title + self.favicon = favicon + } + + public init(apiWebDomainException: Api.WebDomainException) { + switch apiWebDomainException { + case let .webDomainException(data): + self.init(domain: data.domain, url: data.url, title: data.title, favicon: data.favicon) + } + } +} + +public struct AccountWebBrowserSettings: Codable, Equatable { + public let openExternalBrowser: Bool + public let externalExceptions: [AccountWebBrowserException] + public let inAppExceptions: [AccountWebBrowserException] + public let hash: Int64 + + public static var defaultSettings: AccountWebBrowserSettings { + return AccountWebBrowserSettings(openExternalBrowser: false, externalExceptions: [], inAppExceptions: [], hash: 0) + } + + public init(openExternalBrowser: Bool, externalExceptions: [AccountWebBrowserException], inAppExceptions: [AccountWebBrowserException], hash: Int64) { + self.openExternalBrowser = openExternalBrowser + self.externalExceptions = externalExceptions + self.inAppExceptions = inAppExceptions + self.hash = hash + } + + public init(apiWebBrowserSettings: Api.account.WebBrowserSettings, current: AccountWebBrowserSettings?) { + switch apiWebBrowserSettings { + case let .webBrowserSettings(data): + self.init( + openExternalBrowser: (data.flags & (1 << 0)) != 0, + externalExceptions: data.externalExceptions.map(AccountWebBrowserException.init(apiWebDomainException:)), + inAppExceptions: data.inappExceptions.map(AccountWebBrowserException.init(apiWebDomainException:)), + hash: data.hash + ) + case .webBrowserSettingsNotModified: + self = current ?? .defaultSettings + } + } + + public func withUpdatedOpenExternalBrowser(_ openExternalBrowser: Bool) -> AccountWebBrowserSettings { + return AccountWebBrowserSettings(openExternalBrowser: openExternalBrowser, externalExceptions: self.externalExceptions, inAppExceptions: self.inAppExceptions, hash: 0) + } + + public func withAppliedExceptionUpdate(openExternalBrowser: Bool?, delete: Bool, exception: AccountWebBrowserException) -> AccountWebBrowserSettings { + var externalExceptions = self.externalExceptions + var inAppExceptions = self.inAppExceptions + + let removeMatching: (inout [AccountWebBrowserException]) -> Void = { list in + list.removeAll(where: { item in + if !exception.url.isEmpty && item.url == exception.url { + return true + } + return item.domain == exception.domain + }) + } + + if delete { + if let openExternalBrowser { + if openExternalBrowser { + removeMatching(&externalExceptions) + } else { + removeMatching(&inAppExceptions) + } + } else { + removeMatching(&externalExceptions) + removeMatching(&inAppExceptions) + } + } else if let openExternalBrowser { + if openExternalBrowser { + removeMatching(&externalExceptions) + externalExceptions.append(exception) + } else { + removeMatching(&inAppExceptions) + inAppExceptions.append(exception) + } + } + + return AccountWebBrowserSettings(openExternalBrowser: self.openExternalBrowser, externalExceptions: externalExceptions, inAppExceptions: inAppExceptions, hash: 0) + } +} + +private func storeAccountWebBrowserSettings(postbox: Postbox, settings: AccountWebBrowserSettings) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: PreferencesKeys.webBrowserSettings, { _ in + return PreferencesEntry(settings) + }) + } +} + +func _internal_getAccountWebBrowserSettings(postbox: Postbox, network: Network, forceUpdate: Bool = false) -> Signal { + let fetch: (AccountWebBrowserSettings?, Int64) -> Signal = { current, hash in + return network.request(Api.functions.account.getWebBrowserSettings(hash: hash)) + |> retryRequestIfNotFrozen + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + switch result { + case .webBrowserSettingsNotModified: + return .complete() + case .webBrowserSettings: + let settings = AccountWebBrowserSettings(apiWebBrowserSettings: result, current: current) + if let current, settings == current { + return .complete() + } else { + return storeAccountWebBrowserSettings(postbox: postbox, settings: settings) + |> mapToSignal { _ -> Signal in + return .single(settings) + } + } + } + } + } + + return postbox.transaction { transaction -> AccountWebBrowserSettings in + return transaction.getPreferencesEntry(key: PreferencesKeys.webBrowserSettings)?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + } + |> mapToSignal { current -> Signal in + let hash: Int64 = forceUpdate ? 0 : current.hash + return .single(current) + |> then(fetch(current, hash)) + } +} + +public func updateRemoteWebBrowserSettings(postbox: Postbox, network: Network, openExternalBrowser: Bool) -> Signal { + var flags: Int32 = 0 + if openExternalBrowser { + flags |= (1 << 0) + } + return network.request(Api.functions.account.updateWebBrowserSettings(flags: flags)) + |> retryRequestIfNotFrozen + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + return postbox.transaction { transaction -> AccountWebBrowserSettings? in + return transaction.getPreferencesEntry(key: PreferencesKeys.webBrowserSettings)?.get(AccountWebBrowserSettings.self) + } + |> mapToSignal { current -> Signal in + return storeAccountWebBrowserSettings(postbox: postbox, settings: AccountWebBrowserSettings(apiWebBrowserSettings: result, current: current)) + } + } +} + +public func toggleWebBrowserSettingsException(postbox: Postbox, network: Network, openExternalBrowser: Bool?, delete: Bool, url: String) -> Signal { + var flags: Int32 = 0 + var apiOpenExternalBrowser: Api.Bool? + if let openExternalBrowser { + flags |= (1 << 0) + apiOpenExternalBrowser = openExternalBrowser ? .boolFalse : .boolTrue + } + if delete { + flags |= (1 << 1) + } + return network.request(Api.functions.account.toggleWebBrowserSettingsException(flags: flags, openExternalBrowser: apiOpenExternalBrowser, url: url)) + |> retryRequestIfNotFrozen + |> mapToSignal { result -> Signal in + guard let result else { + return .single(false) + } + + return postbox.transaction { transaction -> Bool in + var settings = transaction.getPreferencesEntry(key: PreferencesKeys.webBrowserSettings)?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + var updated = false + for update in result.allUpdates { + switch update { + case let .updateWebBrowserSettings(updateWebBrowserSettingsData): + settings = settings.withUpdatedOpenExternalBrowser((updateWebBrowserSettingsData.flags & (1 << 0)) != 0) + updated = true + case let .updateWebBrowserException(updateWebBrowserExceptionData): + let openExternalBrowser: Bool? + if let value = updateWebBrowserExceptionData.openExternalBrowser { + switch value { + case .boolFalse: + openExternalBrowser = false + case .boolTrue: + openExternalBrowser = true + } + } else { + openExternalBrowser = nil + } + + settings = settings.withAppliedExceptionUpdate( + openExternalBrowser: openExternalBrowser, + delete: (updateWebBrowserExceptionData.flags & (1 << 1)) != 0, + exception: AccountWebBrowserException(apiWebDomainException: updateWebBrowserExceptionData.exception) + ) + updated = true + default: + break + } + } + + if updated { + transaction.updatePreferencesEntry(key: PreferencesKeys.webBrowserSettings, { _ in + return PreferencesEntry(settings) + }) + } + + return updated + } + } +} + +public func deleteWebBrowserSettingsExceptions(postbox: Postbox, network: Network) -> Signal { + return network.request(Api.functions.account.deleteWebBrowserSettingsExceptions()) + |> retryRequestIfNotFrozen + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + return postbox.transaction { transaction -> AccountWebBrowserSettings? in + return transaction.getPreferencesEntry(key: PreferencesKeys.webBrowserSettings)?.get(AccountWebBrowserSettings.self) + } + |> mapToSignal { current -> Signal in + return storeAccountWebBrowserSettings(postbox: postbox, settings: AccountWebBrowserSettings(apiWebBrowserSettings: result, current: current)) + } + } +} 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 d330ae4782..d80793f3b2 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1837,19 +1837,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 { @@ -1979,6 +1966,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: updatedState.addUpdateAttachMenuBots() case let .updateWebViewResultSent(updateWebViewResultSentData): updatedState.addDismissWebView(updateWebViewResultSentData.queryId) + case let .updateJoinChatWebViewDecision(updateJoinChatWebViewDecisionData): + updatedState.addJoinChatWebViewDecision(JoinChatWebViewDecision(peerId: updateJoinChatWebViewDecisionData.peer.peerId, queryId: updateJoinChatWebViewDecisionData.queryId, result: JoinChatWebViewResult(apiResult: updateJoinChatWebViewDecisionData.result))) case .updateConfig: updatedState.reloadConfig() case let .updateMessageExtendedMedia(updateMessageExtendedMediaData): @@ -1996,6 +1985,34 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: let (flags, hash, date, device, location) = (updateNewAuthorizationData.flags, updateNewAuthorizationData.hash, updateNewAuthorizationData.date, updateNewAuthorizationData.device, updateNewAuthorizationData.location) let isUnconfirmed = (flags & (1 << 0)) != 0 updatedState.updateNewAuthorization(isUnconfirmed: isUnconfirmed, hash: hash, date: date ?? 0, device: device ?? "", location: location ?? "") + case let .updateNewBotConnection(updateNewBotConnectionData): + let (flags, botId, date, device, location) = (updateNewBotConnectionData.flags, updateNewBotConnectionData.botId, updateNewBotConnectionData.date, updateNewBotConnectionData.device, updateNewBotConnectionData.location) + updatedState.updateNewBotConnection( + confirmed: (flags & (1 << 0)) != 0, + botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), + date: date, + device: device, + location: location + ) + case let .updateWebBrowserSettings(updateWebBrowserSettingsData): + updatedState.updateWebBrowserSettings(openExternalBrowser: (updateWebBrowserSettingsData.flags & (1 << 0)) != 0) + case let .updateWebBrowserException(updateWebBrowserExceptionData): + let openExternalBrowser: Bool? + if let value = updateWebBrowserExceptionData.openExternalBrowser { + switch value { + case .boolFalse: + openExternalBrowser = false + case .boolTrue: + openExternalBrowser = true + } + } else { + openExternalBrowser = nil + } + updatedState.updateWebBrowserException( + openExternalBrowser: openExternalBrowser, + delete: (updateWebBrowserExceptionData.flags & (1 << 1)) != 0, + exception: AccountWebBrowserException(apiWebDomainException: updateWebBrowserExceptionData.exception) + ) case let .updatePeerWallpaper(updatePeerWallpaperData): updatedState.updateWallpaper(peerId: updatePeerWallpaperData.peer.peerId, wallpaper: updatePeerWallpaperData.wallpaper.flatMap { TelegramWallpaper(apiWallpaper: $0) }) case let .updateStarsBalance(updateStarsBalanceData): @@ -3826,7 +3843,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, .UpdateNewBotConnection, .UpdateWebBrowserSettings, .UpdateWebBrowserException, .UpdateWallpaper, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState, .UpdateEmojiGameInfo: if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty { result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location)) } @@ -3945,7 +3962,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)] = [:] @@ -3971,6 +3987,7 @@ func replayFinalState( var updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:] var updatedEmojiGameInfo: EmojiGameInfo? var recentlyUsedGuestChatBots = Set() + var webBrowserSettingsUpdates: [(AccountWebBrowserSettings) -> AccountWebBrowserSettings] = [] var holesFromPreviousStateMessageIds: [MessageId] = [] var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] @@ -5232,8 +5249,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): @@ -5555,6 +5570,26 @@ func replayFinalState( } else { transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewSessionReviews, itemId: id.rawValue) } + case let .UpdateNewBotConnection(confirmed, botId, date, device, location): + let id = NewBotConnectionReview.Id(botId: botId) + if confirmed { + transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewBotConnectionReviews, itemId: id.rawValue) + } else if let entry = CodableEntry(NewBotConnectionReview( + botId: botId, + device: device, + location: location, + timestamp: date + )) { + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewBotConnectionReviews, item: OrderedItemListEntry(id: id.rawValue, contents: entry), removeTailIfCountExceeds: 200) + } + case let .UpdateWebBrowserSettings(openExternalBrowser): + webBrowserSettingsUpdates.append { settings in + settings.withUpdatedOpenExternalBrowser(openExternalBrowser) + } + case let .UpdateWebBrowserException(openExternalBrowser, delete, exception): + webBrowserSettingsUpdates.append { settings in + settings.withAppliedExceptionUpdate(openExternalBrowser: openExternalBrowser, delete: delete, exception: exception) + } case let .UpdateStarsBalance(peerId, currency, balance): switch currency { case .ton: @@ -6033,6 +6068,16 @@ func replayFinalState( }) }.start() } + + if !webBrowserSettingsUpdates.isEmpty { + transaction.updatePreferencesEntry(key: PreferencesKeys.webBrowserSettings, { current in + var settings = current?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + for update in webBrowserSettingsUpdates { + settings = update(settings) + } + return PreferencesEntry(settings) + }) + } if !updatedWallpapers.isEmpty { for (peerId, wallpaper) in updatedWallpapers { @@ -6186,7 +6231,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 e7e5a1ce7e..101f25a391 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>() } @@ -371,7 +367,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() @@ -486,10 +481,6 @@ public final class AccountStateManager { } func addUpdates(_ updates: Api.Updates) { - let decisions = extractJoinChatWebViewDecisions(from: updates) - if !decisions.isEmpty { - self.joinChatWebViewDecisionsPipe.putNext(decisions) - } self.queue.async { self.updateService?.addUpdates(updates) } @@ -1136,9 +1127,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) } @@ -1280,6 +1268,10 @@ public final class AccountStateManager { self.dismissBotWebViewsPipe.putNext(events.dismissBotWebViews) } + if !events.joinChatWebViewDecisions.isEmpty { + self.joinChatWebViewDecisionsPipe.putNext(events.joinChatWebViewDecisions) + } + if !events.externallyUpdatedPeerId.isEmpty { self.externallyUpdatedPeerIdsPipe.putNext(Array(events.externallyUpdatedPeerId)) } @@ -1718,34 +1710,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 @@ -1966,18 +1931,18 @@ public final class AccountStateManager { } } - public var joinChatWebViewDecisions: Signal<[JoinChatWebViewDecision], NoError> { - return self.impl.signalWith { impl, subscriber in - return impl.joinChatWebViewDecisions.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) - } - } - public var dismissBotWebViews: Signal<[Int64], NoError> { return self.impl.signalWith { impl, subscriber in return impl.dismissBotWebViews.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) } } + public var joinChatWebViewDecisions: Signal<[JoinChatWebViewDecision], NoError> { + return self.impl.signalWith { impl, subscriber in + return impl.joinChatWebViewDecisions.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + var externallyUpdatedPeerIds: Signal<[PeerId], NoError> { return self.impl.signalWith { impl, subscriber in return impl.externallyUpdatedPeerIds.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) @@ -2250,12 +2215,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/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index 087214ebb7..0cc816fc04 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -135,6 +135,7 @@ final class AccountTaskManager { self.reloadAppConfiguration() tasks.add(managedPremiumPromoConfigurationUpdates(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedAutodownloadSettingsUpdates(accountManager: self.accountManager, network: self.stateManager.network).start()) + tasks.add(managedWebBrowserSettingsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) tasks.add(managedTermsOfServiceUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedAppUpdateInfo(network: self.stateManager.network, stateManager: self.stateManager).start()) tasks.add(managedLocalizationUpdatesOperations(accountManager: self.accountManager, postbox: self.stateManager.postbox, network: self.stateManager.network).start()) diff --git a/submodules/TelegramCore/Sources/State/ManagedWebBrowserSettingsUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedWebBrowserSettingsUpdates.swift new file mode 100644 index 0000000000..51ab736b4a --- /dev/null +++ b/submodules/TelegramCore/Sources/State/ManagedWebBrowserSettingsUpdates.swift @@ -0,0 +1,15 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +func managedWebBrowserSettingsUpdates(postbox: Postbox, network: Network) -> Signal { + let poll = Signal { subscriber in + return (_internal_getAccountWebBrowserSettings(postbox: postbox, network: network) + |> ignoreValues).start(completed: { + subscriber.putCompletion() + }) + } + return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift index 3675553590..d20b7f5a00 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedChannelData.swift @@ -273,8 +273,8 @@ public final class CachedChannelData: CachedPeerData { public let starGiftsCount: Int32? public let sendPaidMessageStars: StarsAmount? public let mainProfileTab: TelegramProfileTab? - public let guardBotId: PeerId? - + public let guardBotId: EnginePeer.Id? + public let peerIds: Set public let messageIds: Set public var associatedHistoryMessageId: MessageId? { @@ -364,7 +364,7 @@ public final class CachedChannelData: CachedPeerData { starGiftsCount: Int32?, sendPaidMessageStars: StarsAmount?, mainProfileTab: TelegramProfileTab?, - guardBotId: PeerId? + guardBotId: EnginePeer.Id? ) { self.isNotAccessible = isNotAccessible self.flags = flags @@ -412,19 +412,19 @@ public final class CachedChannelData: CachedPeerData { } if case let .known(linkedDiscussionPeerIdValue) = linkedDiscussionPeerId { - if let linkedDiscussionPeerIdValue = linkedDiscussionPeerIdValue { + if let linkedDiscussionPeerIdValue { peerIds.insert(linkedDiscussionPeerIdValue) } } - - if let invitedBy = invitedBy { + + if let invitedBy { peerIds.insert(invitedBy) } - + if let guardBotId { peerIds.insert(guardBotId) } - + self.peerIds = peerIds var messageIds = Set() @@ -586,9 +586,9 @@ public final class CachedChannelData: CachedPeerData { public func withUpdatedMainProfileTab(_ mainProfileTab: TelegramProfileTab?) -> CachedChannelData { return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, chatTheme: self.chatTheme, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, reactionSettings: self.reactionSettings, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack, verification: self.verification, starGiftsCount: self.starGiftsCount, sendPaidMessageStars: self.sendPaidMessageStars, mainProfileTab: mainProfileTab, guardBotId: self.guardBotId) } - - public func withUpdatedGuardBotId(_ guardBotId: PeerId?) -> CachedChannelData { - return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: self.pendingSuggestions, chatTheme: self.chatTheme, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, reactionSettings: self.reactionSettings, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack, verification: self.verification, starGiftsCount: self.starGiftsCount, sendPaidMessageStars: self.sendPaidMessageStars, mainProfileTab: self.mainProfileTab, guardBotId: guardBotId) + + public func withUpdatedGuardBotId(_ guardBotId: EnginePeer.Id?) -> CachedChannelData { + return CachedChannelData(isNotAccessible: self.isNotAccessible, flags: self.flags, about: self.about, participantsSummary: self.participantsSummary, exportedInvitation: self.exportedInvitation, botInfos: self.botInfos, peerStatusSettings: self.peerStatusSettings, pinnedMessageId: self.pinnedMessageId, stickerPack: self.stickerPack, minAvailableMessageId: self.minAvailableMessageId, migrationReference: self.migrationReference, linkedDiscussionPeerId: self.linkedDiscussionPeerId, peerGeoLocation: self.peerGeoLocation, slowModeTimeout: self.slowModeTimeout, slowModeValidUntilTimestamp: self.slowModeValidUntilTimestamp, hasScheduledMessages: self.hasScheduledMessages, statsDatacenterId: self.statsDatacenterId, invitedBy: self.invitedBy, invitedOn: self.invitedOn, photo: self.photo, activeCall: self.activeCall, callJoinPeerId: self.callJoinPeerId, autoremoveTimeout: self.autoremoveTimeout, pendingSuggestions: pendingSuggestions, chatTheme: self.chatTheme, inviteRequestsPending: self.inviteRequestsPending, sendAsPeerId: self.sendAsPeerId, reactionSettings: self.reactionSettings, membersHidden: self.membersHidden, viewForumAsMessages: self.viewForumAsMessages, wallpaper: self.wallpaper, boostsToUnrestrict: self.boostsToUnrestrict, appliedBoosts: self.appliedBoosts, emojiPack: self.emojiPack, verification: self.verification, starGiftsCount: self.starGiftsCount, sendPaidMessageStars: self.sendPaidMessageStars, mainProfileTab: self.mainProfileTab, guardBotId: guardBotId) } public init(decoder: PostboxDecoder) { @@ -601,7 +601,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 { @@ -733,15 +733,12 @@ public final class CachedChannelData: CachedPeerData { self.verification = decoder.decodeCodable(PeerVerification.self, forKey: "vf") self.mainProfileTab = decoder.decodeCodable(TelegramProfileTab.self, forKey: "mainProfileTab") - - if let guardBotId = decoder.decodeOptionalInt64ForKey("gbid") { - let guardBotPeerId = PeerId(guardBotId) - self.guardBotId = guardBotPeerId - peerIds.insert(guardBotPeerId) - } else { - self.guardBotId = nil + + self.guardBotId = decoder.decodeOptionalInt64ForKey("guardBotId").flatMap(EnginePeer.Id.init) + if let guardBotId = self.guardBotId { + peerIds.insert(guardBotId) } - + self.peerIds = peerIds var messageIds = Set() @@ -949,11 +946,11 @@ public final class CachedChannelData: CachedPeerData { } else { encoder.encodeNil(forKey: "mainProfileTab") } - + if let guardBotId = self.guardBotId { - encoder.encodeInt64(guardBotId.toInt64(), forKey: "gbid") + encoder.encodeInt64(guardBotId.toInt64(), forKey: "guardBotId") } else { - encoder.encodeNil(forKey: "gbid") + encoder.encodeNil(forKey: "guardBotId") } } @@ -1101,13 +1098,15 @@ public final class CachedChannelData: CachedPeerData { if other.verification != self.verification { return false } + if other.mainProfileTab != self.mainProfileTab { return false } + if other.guardBotId != self.guardBotId { return false } - + return true } } 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..ff5665e5f3 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -97,6 +97,7 @@ public struct Namespaces { public static let CloudDisabledChannelStatusEmoji: Int32 = 28 public static let CloudDefaultTagReactions: Int32 = 29 public static let CloudUniqueStarGifts: Int32 = 30 + public static let NewBotConnectionReviews: Int32 = 31 } public struct CachedItemCollection { @@ -328,6 +329,7 @@ private enum PreferencesKeyValues: Int32 { case globalPostSearchState = 46 case savedMusicIds = 47 case emojiGameInfo = 48 + case webBrowserSettings = 49 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -427,15 +429,15 @@ public struct PreferencesKeys { return key }() - public static let chatListFilters: ValueBoxKey = { + public static let webBrowserSettings: ValueBoxKey = { let key = ValueBoxKey(length: 4) - key.setInt32(0, value: PreferencesKeyValues.chatListFilters.rawValue) + key.setInt32(0, value: PreferencesKeyValues.webBrowserSettings.rawValue) return key }() - public static let peersNearby: ValueBoxKey = { + public static let chatListFilters: ValueBoxKey = { let key = ValueBoxKey(length: 4) - key.setInt32(0, value: PreferencesKeyValues.peersNearby.rawValue) + key.setInt32(0, value: PreferencesKeyValues.chatListFilters.rawValue) return key }() diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_NewBotConnectionReview.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_NewBotConnectionReview.swift new file mode 100644 index 0000000000..201059cc3b --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_NewBotConnectionReview.swift @@ -0,0 +1,85 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public final class NewBotConnectionReview: Codable, Equatable { + struct Id { + var rawValue: MemoryBuffer + + init(botId: PeerId) { + let buffer = WriteBuffer() + + var rawBotId = botId.toInt64() + buffer.write(&rawBotId, length: 8) + + self.rawValue = buffer.makeReadBufferAndReset() + } + } + + public let botId: PeerId + public let device: String? + public let location: String? + public let timestamp: Int32? + + public init(botId: PeerId, device: String?, location: String?, timestamp: Int32?) { + self.botId = botId + self.device = device + self.location = location + self.timestamp = timestamp + } + + public static func ==(lhs: NewBotConnectionReview, rhs: NewBotConnectionReview) -> Bool { + if lhs.botId != rhs.botId { + return false + } + if lhs.device != rhs.device { + return false + } + if lhs.location != rhs.location { + return false + } + if lhs.timestamp != rhs.timestamp { + return false + } + return true + } +} + +public func newBotConnectionReviews(postbox: Postbox) -> Signal<[NewBotConnectionReview], NoError> { + let viewKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.NewBotConnectionReviews) + return postbox.combinedView(keys: [viewKey]) + |> mapToSignal { views -> Signal<[NewBotConnectionReview], NoError> in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return .single([]) + } + + var result: [NewBotConnectionReview] = [] + for item in view.items { + guard let item = item.contents.get(NewBotConnectionReview.self) else { + continue + } + result.append(item) + } + + return .single(result) + } +} + +public func addNewBotConnectionReview(postbox: Postbox, item: NewBotConnectionReview) -> Signal { + return postbox.transaction { transaction -> Void in + guard let entry = CodableEntry(item) else { + return + } + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewBotConnectionReviews, item: OrderedItemListEntry(id: NewBotConnectionReview.Id(botId: item.botId).rawValue, contents: entry), removeTailIfCountExceeds: 200) + } + |> ignoreValues +} + +public func removeNewBotConnectionReviews(postbox: Postbox, botIds: [PeerId]) -> Signal { + return postbox.transaction { transaction -> Void in + for botId in botIds { + transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.NewBotConnectionReviews, itemId: NewBotConnectionReview.Id(botId: botId).rawValue) + } + } + |> ignoreValues +} 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/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index 66fb51e960..f3d2a94ecd 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -46,7 +46,7 @@ public struct BotUserInfoFlags: OptionSet { public static let forumManagedByUser = BotUserInfoFlags(rawValue: (1 << 9)) public static let canManageBots = BotUserInfoFlags(rawValue: (1 << 10)) public static let isGuestChat = BotUserInfoFlags(rawValue: (1 << 11)) - public static let botGuard = BotUserInfoFlags(rawValue: (1 << 12)) + public static let isGuardBot = BotUserInfoFlags(rawValue: (1 << 12)) } public struct BotUserInfo: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/BotConnectionReviews.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/BotConnectionReviews.swift new file mode 100644 index 0000000000..10a3da7c67 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/BotConnectionReviews.swift @@ -0,0 +1,35 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi + +func _internal_confirmBotConnectionReview(account: Account, botId: PeerId) -> Signal { + return account.network.request(Api.functions.account.getConnectedBots()) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result else { + return .complete() + } + + return account.postbox.transaction { transaction -> Api.InputUser? in + switch result { + case let .connectedBots(connectedBotsData): + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: connectedBotsData.users)) + } + return transaction.getPeer(botId).flatMap(apiInputUser) + } + |> mapToSignal { inputUser -> Signal in + guard let inputUser else { + return .complete() + } + return account.network.request(Api.functions.account.confirmBotConnection(botId: inputUser)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> ignoreValues + } + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index 9152d408d9..cb64547812 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -282,6 +282,10 @@ public extension TelegramEngine { return _internal_setAccountConnectedBot(account: self.account, bot: bot) } + public func confirmBotConnectionReview(botId: PeerId) -> Signal { + return _internal_confirmBotConnectionReview(account: self.account, botId: botId) + } + public func updateBusinessIntro(intro: TelegramBusinessIntro?) -> Signal { return _internal_updateBusinessIntro(account: self.account, intro: intro) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/OrderedListsData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/OrderedListsData.swift index e9bb29c45e..d7d2592844 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/OrderedListsData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/OrderedListsData.swift @@ -45,6 +45,26 @@ public extension TelegramEngine.EngineData.Item { } enum OrderedLists { + public struct NewBotConnectionReviews: TelegramEngineDataItem, PostboxViewDataItem { + public typealias Result = [NewBotConnectionReview] + + public init() { + } + + var key: PostboxViewKey { + return .orderedItemList(id: Namespaces.OrderedItemList.NewBotConnectionReviews) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? OrderedItemListView else { + preconditionFailure() + } + return view.items.compactMap { item in + return item.contents.get(NewBotConnectionReview.self) + } + } + } + public struct ListItems: TelegramEngineDataItem, PostboxViewDataItem { public typealias Result = [OrderedItemListEntry] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 08dd69f36f..93d1d77c46 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -284,6 +284,9 @@ func _internal_requestWebView(postbox: Postbox, network: Network, stateManager: if (webViewFlags & (1 << 2)) != 0 { resultFlags.insert(.fullScreen) } + if (webViewFlags & (1 << 3)) != 0 { + resultFlags.insert(.sameOrigin) + } let keepAlive: Signal? if let queryId { keepAlive = keepWebViewSignal(network: network, stateManager: stateManager, flags: flags, peer: inputPeer, monoforumPeerId: monoforumPeerId, bot: inputBot, queryId: queryId, replyToMessageId: replyToMessageId, threadId: threadId, sendAs: nil) @@ -719,7 +722,10 @@ func _internal_removeChatManagingBot(account: Account, chatId: EnginePeer.Id) -> excludePeers: excludePeers, exclude: connectedBot.recipients.exclude ), - rights: connectedBot.rights + rights: connectedBot.rights, + device: connectedBot.device, + date: connectedBot.date, + location: connectedBot.location )) } else { return current diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift index f3597a1d1b..921ab28308 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift @@ -24,6 +24,9 @@ func pollCloudMediaToInputMedia(_ media: Media) -> Api.InputMedia? { } else { return .inputMediaGeoPoint(.init(geoPoint: geoPoint)) } + } else if let webpage = media as? TelegramMediaWebpage, let url = webpage.content.url { + let flags: Int32 = 1 << 2 + return .inputMediaWebPage(.init(flags: flags, url: url)) } return nil } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index aaf771f1e3..b3c22b7c78 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -1085,11 +1085,17 @@ public final class TelegramAccountConnectedBot: Codable, Equatable { public let id: PeerId public let recipients: TelegramBusinessRecipients public let rights: TelegramBusinessBotRights + public let device: String? + public let date: Int32? + public let location: String? - public init(id: PeerId, recipients: TelegramBusinessRecipients, rights: TelegramBusinessBotRights) { + public init(id: PeerId, recipients: TelegramBusinessRecipients, rights: TelegramBusinessBotRights, device: String? = nil, date: Int32? = nil, location: String? = nil) { self.id = id self.recipients = recipients self.rights = rights + self.device = device + self.date = date + self.location = location } public static func ==(lhs: TelegramAccountConnectedBot, rhs: TelegramAccountConnectedBot) -> Bool { @@ -1105,8 +1111,34 @@ public final class TelegramAccountConnectedBot: Codable, Equatable { if lhs.rights != rhs.rights { return false } + if lhs.device != rhs.device { + return false + } + if lhs.date != rhs.date { + return false + } + if lhs.location != rhs.location { + return false + } return true } + + fileprivate func preservingConnectionMetadata(from current: TelegramAccountConnectedBot?) -> TelegramAccountConnectedBot { + guard let current, current.id == self.id else { + return self + } + if self.device != nil || self.date != nil || self.location != nil { + return self + } + return TelegramAccountConnectedBot( + id: self.id, + recipients: self.recipients, + rights: self.rights, + device: current.device, + date: current.date, + location: current.location + ) + } } public struct TelegramBusinessBotRights: OptionSet, Codable { @@ -1251,7 +1283,7 @@ public func _internal_setAccountConnectedBot(account: Account, bot: TelegramAcco return account.postbox.transaction { transaction in transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in var current = (current as? CachedUserData) ?? CachedUserData() - current = current.withUpdatedConnectedBot(bot) + current = current.withUpdatedConnectedBot(bot?.preservingConnectionMetadata(from: current.connectedBot)) return current }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelSendRestriction.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelSendRestriction.swift index 76bfdab15a..53fcc16b3d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelSendRestriction.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelSendRestriction.swift @@ -30,23 +30,46 @@ public enum UpdateChannelJoinRequestError { case generic } -func _internal_toggleChannelJoinRequest(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, peerId: PeerId, enabled: Bool, guardBotId: PeerId? = nil) -> Signal { +func _internal_toggleChannelJoinRequest(postbox: Postbox, network: Network, accountStateManager: AccountStateManager, peerId: PeerId, enabled: Bool, guardBotId: PeerId?, applyToInvites: Bool, clearGuardBot: Bool) -> Signal { + let updatedGuardBotId: PeerId? + let shouldUpdateGuardBotId: Bool + if clearGuardBot { + updatedGuardBotId = nil + shouldUpdateGuardBotId = true + } else if let guardBotId { + updatedGuardBotId = guardBotId + shouldUpdateGuardBotId = true + } else { + updatedGuardBotId = nil + shouldUpdateGuardBotId = false + } + return postbox.transaction { transaction -> (Peer?, Api.InputUser?) in - let peer = transaction.getPeer(peerId) - var guardBot: Api.InputUser? - if let guardBotId, let botPeer = transaction.getPeer(guardBotId) { - guardBot = apiInputUser(botPeer) + let guardBot: Api.InputUser? + if clearGuardBot { + guardBot = .inputUserEmpty + } else if let guardBotId { + guardBot = transaction.getPeer(guardBotId).flatMap(apiInputUser) + } else { + guardBot = nil } - return (peer, guardBot) + return (transaction.getPeer(peerId), guardBot) } |> castError(UpdateChannelJoinRequestError.self) - |> mapToSignal { (peer, guardBot) in + |> mapToSignal { result in + let (peer, guardBot) = result guard let peer = peer, let inputChannel = apiInputChannel(peer) else { return .fail(.generic) } + if guardBotId != nil && guardBot == nil { + return .fail(.generic) + } var flags: Int32 = 0 if guardBot != nil { - flags |= (1 << 0) + flags |= 1 << 0 + } + if applyToInvites { + flags |= 1 << 1 } return network.request(Api.functions.channels.toggleJoinRequest(flags: flags, channel: inputChannel, enabled: enabled ? .boolTrue : .boolFalse, guardBot: guardBot)) |> `catch` { _ -> Signal in @@ -54,18 +77,21 @@ func _internal_toggleChannelJoinRequest(postbox: Postbox, network: Network, acco } |> mapToSignal { updates -> Signal in accountStateManager.addUpdates(updates) - return postbox.transaction { transaction in - transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in - if let current = current as? CachedChannelData { - return current.withUpdatedGuardBotId(guardBotId) - } else { - return current - } - }) + + if shouldUpdateGuardBotId { + return postbox.transaction { transaction -> Void in + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in + guard let current = current as? CachedChannelData else { + return current + } + return current.withUpdatedGuardBotId(updatedGuardBotId) + }) + } + |> castError(UpdateChannelJoinRequestError.self) + |> ignoreValues } - |> ignoreValues - |> castError(UpdateChannelJoinRequestError.self) + + return .complete() } } } - diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift index 01d8f00167..938ed2a358 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift @@ -12,68 +12,16 @@ public enum JoinChannelError { case inviteRequestSent } -public enum JoinChannelOutcome { +public enum JoinChannelResult { case joined(RenderedChannelParticipant?) - case webView(botId: PeerId, url: String, queryId: Int64) + case webView(JoinChatWebView) } -public enum JoinChatBotDecision: Equatable { - case approved - case declined - case queued - case webView(url: String) -} - -public struct JoinChatWebViewDecision: Equatable { - public let peerId: PeerId - public let queryId: Int64 - public let result: JoinChatBotDecision - - public init(peerId: PeerId, queryId: Int64, result: JoinChatBotDecision) { - self.peerId = peerId - self.queryId = queryId - self.result = result - } -} - -func extractJoinChatWebViewDecisions(from updates: Api.Updates) -> [JoinChatWebViewDecision] { - var apiUpdates: [Api.Update] = [] - switch updates { - case let .updates(data): - apiUpdates = data.updates - case let .updatesCombined(data): - apiUpdates = data.updates - case let .updateShort(data): - apiUpdates = [data.update] - default: - return [] - } - var result: [JoinChatWebViewDecision] = [] - for update in apiUpdates { - if case let .updateJoinChatWebViewDecision(data) = update { - let decision: JoinChatBotDecision - switch data.result { - case .joinChatBotResultApproved: - decision = .approved - case .joinChatBotResultDeclined: - decision = .declined - case .joinChatBotResultQueued: - decision = .queued - case let .joinChatBotResultWebView(webData): - decision = .webView(url: webData.url) - } - result.append(JoinChatWebViewDecision(peerId: data.peer.peerId, queryId: data.queryId, result: decision)) - } - } - return result -} - -func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> Signal { +func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> Signal { return account.postbox.loadedPeerWithId(peerId) |> take(1) |> castError(JoinChannelError.self) - |> mapToSignal { peer -> Signal in - + |> mapToSignal { peer -> Signal in let request: Signal if let hash = hash { request = account.network.request(Api.functions.messages.importChatInvite(hash: hash)) @@ -96,71 +44,76 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S return .generic } } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in + let updates: Api.Updates switch result { - case let .chatInviteJoinResultOk(result): - account.stateManager.addUpdates(result.updates) - - let channels = result.updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) }.compactMap(apiInputChannel) - - if let inputChannel = channels.first { - return account.network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: .inputPeerSelf)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { result -> Signal in - guard let result = result else { + case let .chatInviteJoinResultOk(data): + updates = data.updates + case let .chatInviteJoinResultWebView(data): + switch data.webview { + case let .webViewResultUrl(urlData): + let botPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(data.botId)) + return account.postbox.transaction { transaction -> Signal in + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: data.users)) + guard let botPeer = transaction.getPeer(botPeerId) else { return .fail(.generic) } - return account.postbox.transaction { transaction -> JoinChannelOutcome in - var peers: [EnginePeer.Id: EnginePeer] = [:] - var presences: [PeerId: PeerPresence] = [:] - guard let peer = transaction.getPeer(account.peerId) else { - return .joined(nil) - } - peers[account.peerId] = EnginePeer(peer) - if let presence = transaction.getPeerPresence(peerId: account.peerId) { - presences[account.peerId] = presence - } - let updatedParticipant: ChannelParticipant - switch result { - case let .channelParticipant(channelParticipantData): - let participant = channelParticipantData.participant - updatedParticipant = ChannelParticipant(apiParticipant: participant) - } - if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant { - if let adminInfo = maybeAdminInfo { - if let peer = transaction.getPeer(adminInfo.promotedBy) { - peers[peer.id] = EnginePeer(peer) - } - } - } - - return .joined(RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(peer), peers: peers, presences: presences)) - } - |> castError(JoinChannelError.self) - } - } else { - return .fail(.generic) - } - case let .chatInviteJoinResultWebView(data): - return account.postbox.transaction { transaction -> Void in - updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(transaction: transaction, chats: [], users: data.users)) - } - |> castError(JoinChannelError.self) - |> map { _ -> JoinChannelOutcome in - switch data.webview { - case let .webViewResultUrl(webViewResultUrl): - return .webView(botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(data.botId)), url: webViewResultUrl.url, queryId: webViewResultUrl.queryId ?? 0) + return .single(.webView(JoinChatWebView(botPeer: EnginePeer(botPeer), url: urlData.url, queryId: urlData.queryId ?? 0, peerId: peerId))) } + |> castError(JoinChannelError.self) + |> switchToLatest } } - } - |> afterCompleted { + + account.stateManager.addUpdates(updates) if hash == nil { let _ = _internal_requestRecommendedChannels(account: account, peerId: peerId, forceUpdate: true).startStandalone() } + + let channels = updates.chats.compactMap { parseTelegramGroupOrChannel(chat: $0) }.compactMap(apiInputChannel) + + if let inputChannel = channels.first { + return account.network.request(Api.functions.channels.getParticipant(channel: inputChannel, participant: .inputPeerSelf)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + guard let result = result else { + return .fail(.generic) + } + return account.postbox.transaction { transaction -> JoinChannelResult in + var peers: [EnginePeer.Id: EnginePeer] = [:] + var presences: [PeerId: PeerPresence] = [:] + guard let peer = transaction.getPeer(account.peerId) else { + return .joined(nil) + } + peers[account.peerId] = EnginePeer(peer) + if let presence = transaction.getPeerPresence(peerId: account.peerId) { + presences[account.peerId] = presence + } + let updatedParticipant: ChannelParticipant + switch result { + case let .channelParticipant(channelParticipantData): + let participant = channelParticipantData.participant + updatedParticipant = ChannelParticipant(apiParticipant: participant) + } + if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant { + if let adminInfo = maybeAdminInfo { + if let peer = transaction.getPeer(adminInfo.promotedBy) { + peers[peer.id] = EnginePeer(peer) + } + } + } + + return .joined(RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(peer), peers: peers, presences: presences)) + } + |> castError(JoinChannelError.self) + } + } else { + return .fail(.generic) + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift index 3c4f10db2c..f05f549fef 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinLink.swift @@ -17,6 +17,64 @@ public enum JoinLinkError { case flood } +public enum JoinChatWebViewResult { + case approved + case declined + case queued + case webView(url: String) +} + +extension JoinChatWebViewResult { + init(apiResult: Api.JoinChatBotResult) { + switch apiResult { + case .joinChatBotResultApproved: + self = .approved + case .joinChatBotResultDeclined: + self = .declined + case .joinChatBotResultQueued: + self = .queued + case let .joinChatBotResultWebView(data): + self = .webView(url: data.url) + } + } +} + +public struct JoinChatWebView { + public let botPeer: EnginePeer + public let url: String + public let queryId: Int64 + public let peerId: EnginePeer.Id? + + public init(botPeer: EnginePeer, url: String, queryId: Int64, peerId: EnginePeer.Id?) { + self.botPeer = botPeer + self.url = url + self.queryId = queryId + self.peerId = peerId + } +} + +public struct JoinChatWebViewDecision { + public let peerId: EnginePeer.Id + public let queryId: Int64 + public let result: JoinChatWebViewResult + + public init(peerId: EnginePeer.Id, queryId: Int64, result: JoinChatWebViewResult) { + self.peerId = peerId + self.queryId = queryId + self.result = result + } +} + +public enum JoinLinkResult { + case joined(EnginePeer?) + case webView(JoinChatWebView) +} + +enum InternalJoinLinkResult { + case joined(PeerId?) + case webView(JoinChatWebView) +} + func apiUpdatesGroups(_ updates: Api.Updates) -> [Api.Chat] { switch updates { case let .updates(updatesData): @@ -62,13 +120,7 @@ public enum ExternalJoiningChatState { case peek(EnginePeer, Int32) } -public enum JoinChatInteractivelyOutcome { - case joined(PeerId?) - case webView(botId: PeerId, url: String, queryId: Int64) -} - -func _internal_joinChatInteractively(with hash: String, account: Account) -> Signal { - let accountPeerId = account.peerId +func _internal_joinChatInteractively(with hash: String, account: Account) -> Signal { return account.network.request(Api.functions.messages.importChatInvite(hash: hash), automaticFloodWait: false) |> mapError { error -> JoinLinkError in switch error.errorDescription { @@ -86,11 +138,12 @@ func _internal_joinChatInteractively(with hash: String, account: Account) -> Sig } } } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in switch result { - case let .chatInviteJoinResultOk(result): - account.stateManager.addUpdates(result.updates) - if let peerId = apiUpdatesGroups(result.updates).first?.peerId { + case let .chatInviteJoinResultOk(data): + let updates = data.updates + account.stateManager.addUpdates(updates) + if let peerId = apiUpdatesGroups(updates).first?.peerId { return account.postbox.multiplePeersView([peerId]) |> castError(JoinLinkError.self) |> filter { view in @@ -98,22 +151,24 @@ func _internal_joinChatInteractively(with hash: String, account: Account) -> Sig } |> take(1) |> map { _ in - return JoinChatInteractivelyOutcome.joined(peerId) + return .joined(peerId) } |> timeout(5.0, queue: Queue.concurrentDefaultQueue(), alternate: .single(.joined(nil)) |> castError(JoinLinkError.self)) } return .single(.joined(nil)) case let .chatInviteJoinResultWebView(data): - return account.postbox.transaction { transaction -> Void in - let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [], users: data.users) - updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) - } - |> castError(JoinLinkError.self) - |> map { _ -> JoinChatInteractivelyOutcome in - switch data.webview { - case let .webViewResultUrl(webViewResultUrl): - return .webView(botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(data.botId)), url: webViewResultUrl.url, queryId: webViewResultUrl.queryId ?? 0) + switch data.webview { + case let .webViewResultUrl(urlData): + let botPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(data.botId)) + return account.postbox.transaction { transaction -> Signal in + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: data.users)) + guard let botPeer = transaction.getPeer(botPeerId) else { + return .fail(.generic) + } + return .single(.webView(JoinChatWebView(botPeer: EnginePeer(botPeer), url: urlData.url, queryId: urlData.queryId ?? 0, peerId: nil))) } + |> castError(JoinLinkError.self) + |> switchToLatest } } } 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/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 8ac472c589..face818b04 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -514,8 +514,8 @@ public extension TelegramEngine { return _internal_toggleChannelJoinToSend(postbox: self.account.postbox, network: self.account.network, accountStateManager: self.account.stateManager, peerId: peerId, enabled: enabled) } - public func toggleChannelJoinRequest(peerId: PeerId, enabled: Bool, guardBotId: PeerId? = nil) -> Signal { - return _internal_toggleChannelJoinRequest(postbox: self.account.postbox, network: self.account.network, accountStateManager: self.account.stateManager, peerId: peerId, enabled: enabled, guardBotId: guardBotId) + public func toggleChannelJoinRequest(peerId: PeerId, enabled: Bool, guardBotId: PeerId? = nil, applyToInvites: Bool = false, clearGuardBot: Bool = false) -> Signal { + return _internal_toggleChannelJoinRequest(postbox: self.account.postbox, network: self.account.network, accountStateManager: self.account.stateManager, peerId: peerId, enabled: enabled, guardBotId: guardBotId, applyToInvites: applyToInvites, clearGuardBot: clearGuardBot) } public func toggleAntiSpamProtection(peerId: PeerId, enabled: Bool) -> Signal { @@ -538,7 +538,7 @@ public extension TelegramEngine { return _internal_updateGroupSpecificEmojiset(postbox: self.account.postbox, network: self.account.network, peerId: peerId, info: info) } - public func joinChannel(peerId: PeerId, hash: String?) -> Signal { + public func joinChannel(peerId: PeerId, hash: String?) -> Signal { return _internal_joinChannel(account: self.account, peerId: peerId, hash: hash) } @@ -843,27 +843,27 @@ public extension TelegramEngine { } } - public enum JoinChatInteractivelyResult { - case joined(EnginePeer?) - case webView(botId: EnginePeer.Id, url: String, queryId: Int64) - } - - public func joinChatInteractively(with hash: String) -> Signal { + public func joinChatInteractively(with hash: String) -> Signal { let account = self.account return _internal_joinChatInteractively(with: hash, account: self.account) - |> mapToSignal { outcome -> Signal in - switch outcome { - case let .joined(id): - guard let id = id else { - return .single(.joined(nil)) - } - return account.postbox.transaction { transaction -> JoinChatInteractivelyResult in - return .joined(transaction.getPeer(id).flatMap(EnginePeer.init)) - } - |> castError(JoinLinkError.self) - case let .webView(botId, url, queryId): - return .single(.webView(botId: botId, url: url, queryId: queryId)) + |> mapToSignal { result -> Signal in + let id: PeerId? + switch result { + case let .joined(peerId): + id = peerId + case let .webView(webView): + return .single(.webView(webView)) } + guard let id = id else { + return .single(.joined(nil)) + } + return account.postbox.transaction { transaction -> EnginePeer? in + return transaction.getPeer(id).flatMap(EnginePeer.init) + } + |> map { peer -> JoinLinkResult in + return .joined(peer) + } + |> castError(JoinLinkError.self) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index df96b4ff59..15b7bfbd8a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -258,11 +258,14 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee if let apiBot = connectedBots.first { switch apiBot { case let .connectedBot(connectedBotData): - let (botId, recipients, rights) = (connectedBotData.botId, connectedBotData.recipients, connectedBotData.rights) + let (botId, recipients, rights, device, date, location) = (connectedBotData.botId, connectedBotData.recipients, connectedBotData.rights, connectedBotData.device, connectedBotData.date, connectedBotData.location) mappedConnectedBot = TelegramAccountConnectedBot( id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), recipients: TelegramBusinessRecipients(apiValue: recipients), - rights: TelegramBusinessBotRights(apiValue: rights) + rights: TelegramBusinessBotRights(apiValue: rights), + device: device, + date: date, + location: location ) } } @@ -675,8 +678,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee switch fullChat { case let .channelFull(channelFullData): - let (flags, flags2, about, participantsCount, adminsCount, kickedCount, bannedCount, chatPhoto, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, defaultSendAs, allowedReactions, reactionsLimit, wallpaper, appliedBoosts, boostsUnrestrict, emojiSet, verification, starGiftsCount, sendPaidMessageStars, mainTab) = (channelFullData.flags, channelFullData.flags2, channelFullData.about, channelFullData.participantsCount, channelFullData.adminsCount, channelFullData.kickedCount, channelFullData.bannedCount, channelFullData.chatPhoto, channelFullData.exportedInvite, channelFullData.botInfo, channelFullData.migratedFromChatId, channelFullData.migratedFromMaxId, channelFullData.pinnedMsgId, channelFullData.stickerset, channelFullData.availableMinId, channelFullData.linkedChatId, channelFullData.location, channelFullData.slowmodeSeconds, channelFullData.slowmodeNextSendDate, channelFullData.statsDc, channelFullData.call, channelFullData.ttlPeriod, channelFullData.pendingSuggestions, channelFullData.groupcallDefaultJoinAs, channelFullData.themeEmoticon, channelFullData.requestsPending, channelFullData.defaultSendAs, channelFullData.availableReactions, channelFullData.reactionsLimit, channelFullData.wallpaper, channelFullData.boostsApplied, channelFullData.boostsUnrestrict, channelFullData.emojiset, channelFullData.botVerification, channelFullData.stargiftsCount, channelFullData.sendPaidMessagesStars, channelFullData.mainTab) - let guardBotIdRaw = channelFullData.guardBotId + let (flags, flags2, about, participantsCount, adminsCount, kickedCount, bannedCount, chatPhoto, apiExportedInvite, apiBotInfos, migratedFromChatId, migratedFromMaxId, pinnedMsgId, stickerSet, minAvailableMsgId, linkedChatId, location, slowmodeSeconds, slowmodeNextSendDate, statsDc, inputCall, ttl, pendingSuggestions, groupcallDefaultJoinAs, themeEmoticon, requestsPending, defaultSendAs, allowedReactions, reactionsLimit, wallpaper, appliedBoosts, boostsUnrestrict, emojiSet, verification, starGiftsCount, sendPaidMessageStars, mainTab, guardBotId) = (channelFullData.flags, channelFullData.flags2, channelFullData.about, channelFullData.participantsCount, channelFullData.adminsCount, channelFullData.kickedCount, channelFullData.bannedCount, channelFullData.chatPhoto, channelFullData.exportedInvite, channelFullData.botInfo, channelFullData.migratedFromChatId, channelFullData.migratedFromMaxId, channelFullData.pinnedMsgId, channelFullData.stickerset, channelFullData.availableMinId, channelFullData.linkedChatId, channelFullData.location, channelFullData.slowmodeSeconds, channelFullData.slowmodeNextSendDate, channelFullData.statsDc, channelFullData.call, channelFullData.ttlPeriod, channelFullData.pendingSuggestions, channelFullData.groupcallDefaultJoinAs, channelFullData.themeEmoticon, channelFullData.requestsPending, channelFullData.defaultSendAs, channelFullData.availableReactions, channelFullData.reactionsLimit, channelFullData.wallpaper, channelFullData.boostsApplied, channelFullData.boostsUnrestrict, channelFullData.emojiset, channelFullData.botVerification, channelFullData.stargiftsCount, channelFullData.sendPaidMessagesStars, channelFullData.mainTab, channelFullData.guardBotId) var channelFlags = CachedChannelFlags() if (flags & (1 << 3)) != 0 { channelFlags.insert(.canDisplayParticipants) @@ -900,6 +902,8 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let mappedChatTheme: ChatTheme? = themeEmoticon.flatMap { .emoticon($0) } + let mappedGuardBotId = guardBotId.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } + return previous.withUpdatedFlags(channelFlags) .withUpdatedAbout(about) .withUpdatedParticipantsSummary(CachedChannelParticipantsSummary(memberCount: participantsCount, adminCount: adminsCount, bannedCount: bannedCount, kickedCount: kickedCount)) @@ -936,7 +940,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee .withUpdatedStarGiftsCount(starGiftsCount) .withUpdatedSendPaidMessageStars(mappedSendPaidMessageStars) .withUpdatedMainProfileTab(mappedMainProfileTab) - .withUpdatedGuardBotId(guardBotIdRaw.flatMap { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }) + .withUpdatedGuardBotId(mappedGuardBotId) }) if let minAvailableMessageId = minAvailableMessageId, minAvailableMessageIdUpdated { 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 5f27b85d41..a58a26756c 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/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index efbb638613..703598e887 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -208,6 +208,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case copyProtectionTips = 86 case aiTextProcessingStyleSelectionTips = 87 case savedMessagesChatListView = 88 + case guestChatMessageTooltip = 89 var key: EngineDataBuffer { let v = EngineDataBuffer(length: 4) @@ -538,6 +539,10 @@ private struct ApplicationSpecificNoticeKeys { return EngineNoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.businessBotMessageTooltip.key) } + static func guestChatMessageTooltip() -> EngineNoticeEntryKey { + return EngineNoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.guestChatMessageTooltip.key) + } + static func captionAboveMediaTooltip() -> EngineNoticeEntryKey { return EngineNoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.captionAboveMediaTooltip.key) } @@ -2264,6 +2269,33 @@ public struct ApplicationSpecificNotice { } } + public static func getGuestChatMessageTooltip(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.guestChatMessageTooltip())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementGuestChatMessageTooltip(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.guestChatMessageTooltip())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = EngineCodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.guestChatMessageTooltip(), entry) + } + + return Int(previousValue) + } + } + public static func getCaptionAboveMediaTooltip(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Int32 in if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.captionAboveMediaTooltip())?.get(ApplicationSpecificCounterNotice.self) { 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..6e695b124b 100644 --- a/submodules/TelegramPermissionsUI/BUILD +++ b/submodules/TelegramPermissionsUI/BUILD @@ -17,16 +17,16 @@ swift_library( "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AccountContext:AccountContext", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", "//submodules/TextFormat:TextFormat", "//submodules/Markdown:Markdown", "//submodules/TelegramPermissions:TelegramPermissions", "//submodules/DeviceAccess:DeviceAccess", - "//submodules/PeersNearbyIconNode:PeersNearbyIconNode", - "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/AppBundle:AppBundle", - "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/TelegramUI/Components/ButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift index 9843334806..0ca0982ada 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionContentNode.swift @@ -6,9 +6,9 @@ import TelegramCore import TelegramPresentationData import TextFormat import TelegramPermissions -import PeersNearbyIconNode -import SolidRoundedButtonNode -import PresentationDataUtils +import ComponentFlow +import ButtonComponent +import ComponentDisplayAdapters import Markdown import AnimatedStickerNode import TelegramAnimatedStickerNode @@ -38,18 +38,18 @@ 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 private let textNode: ImmediateTextNode - private let actionButton: SolidRoundedButtonNode + private let actionButton = ComponentView() private let footerNode: ImmediateTextNode private let privacyPolicyButton: HighlightableButtonNode private let icon: PermissionContentIcon private var title: String private var text: String + private let buttonTitle: String public var buttonAction: (() -> Void)? public var openPrivacyPolicy: (() -> Void)? @@ -67,6 +67,7 @@ public final class PermissionContentNode: ASDisplayNode { self.icon = icon self.title = title self.text = text + self.buttonTitle = buttonTitle self.iconNode = ASImageNode() self.iconNode.isLayerBacked = true @@ -78,13 +79,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 } @@ -106,8 +101,7 @@ public final class PermissionContentNode: ASDisplayNode { self.textNode.maximumNumberOfLines = 0 self.textNode.displaysAsynchronously = false self.textNode.isAccessibilityElement = true - - self.actionButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), glass: true, height: 52.0, cornerRadius: 26.0, isShimmering: true) + self.textNode.lineSpacing = 0.1 self.footerNode = ImmediateTextNode() self.footerNode.textAlignment = .center @@ -135,7 +129,6 @@ public final class PermissionContentNode: ASDisplayNode { let link = MarkdownAttributeSet(font: Font.regular(16.0), textColor: theme.list.itemAccentColor, additionalAttributes: [TelegramTextAttributes.URL: ""]) self.textNode.attributedText = parseMarkdownIntoAttributedString(text.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: secondaryText ? .natural : .center) - self.actionButton.title = buttonTitle self.privacyPolicyButton.isHidden = openPrivacyPolicy == nil if let subtitle = subtitle { @@ -149,19 +142,13 @@ 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) self.addSubnode(self.textNode) - self.addSubnode(self.actionButton) self.addSubnode(self.footerNode) self.addSubnode(self.privacyPolicyButton) - self.actionButton.pressed = { [weak self] in - self?.buttonAction?() - } - self.privacyPolicyButton.addTarget(self, action: #selector(self.privacyPolicyPressed), forControlEvents: .touchUpInside) } @@ -234,7 +221,33 @@ public final class PermissionContentNode: ASDisplayNode { let textSize = self.textNode.updateLayout(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude)) let buttonInset: CGFloat = 16.0 let buttonWidth = min(size.width, size.height) - buttonInset * 2.0 - insets.left - insets.right - let buttonHeight = self.actionButton.updateLayout(width: buttonWidth, transition: transition) + let buttonSize = self.actionButton.update( + transition: ComponentTransition(transition), + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: self.theme.list.itemCheckColors.fillColor, + foreground: self.theme.list.itemCheckColors.foregroundColor, + pressedColor: self.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 26.0, + isShimmering: true + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Text( + text: self.buttonTitle, + font: Font.semibold(17.0), + color: self.theme.list.itemCheckColors.foregroundColor + )) + ), + action: { [weak self] in + self?.buttonAction?() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: 52.0) + ) + let buttonHeight = buttonSize.height let footerSize = self.footerNode.updateLayout(CGSize(width: size.width - smallerSidePadding * 2.0, height: .greatestFiniteMagnitude)) let privacyButtonSize = self.privacyPolicyButton.measure(CGSize(width: size.width - sidePadding * 2.0, height: .greatestFiniteMagnitude)) @@ -255,11 +268,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 +283,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 +307,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) @@ -310,7 +314,15 @@ public final class PermissionContentNode: ASDisplayNode { transition.updateFrame(node: self.titleNode, frame: titleFrame) transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame) transition.updateFrame(node: self.textNode, frame: textFrame) - transition.updateFrame(node: self.actionButton, frame: buttonFrame) + if let buttonView = self.actionButton.view { + if buttonView.superview == nil { + self.view.addSubview(buttonView) + } + buttonView.isAccessibilityElement = true + buttonView.accessibilityLabel = self.buttonTitle + buttonView.accessibilityTraits = [.button] + transition.updateFrame(view: buttonView, frame: buttonFrame) + } transition.updateFrame(node: self.footerNode, frame: footerFrame) transition.updateFrame(node: self.privacyPolicyButton, frame: privacyButtonFrame) 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..37acda80e1 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 @@ -344,6 +342,8 @@ public enum PresentationResourceKey: Int32 { case storyViewListLikeIcon case navigationPostStoryIcon case navigationSortIcon + case navigationBackIcon + case navigationCloseIcon case chatReplyBackgroundTemplateIncomingImage case chatReplyBackgroundTemplateOutgoingDashedImage 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/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift index aa89cf5558..eec3004928 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift @@ -45,7 +45,7 @@ public struct PresentationResourcesRootController { public static func navigationShareIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.rootController.navigationBar.buttonColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlColor) }) // return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, generateShareButtonImage) } @@ -200,6 +200,18 @@ public struct PresentationResourcesRootController { }) } + public static func navigationBackIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationBackIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Navigation/Back"), color: .white) + }) + } + + public static func navigationCloseIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationCloseIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Navigation/Close"), color: .white) + }) + } + public static func callListCallIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.callListCallIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Call List/NewCallListIcon"), color: theme.rootController.navigationBar.accentTextColor) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 15cc9e0b4b..c90687cf93 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1924,8 +1924,15 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } else if let dice = media as? TelegramMediaDice, let gameOutcome = dice.gameOutcome { if let value = dice.value { + let isAccountPeerOutcome: Bool + if let forwardInfo = message.forwardInfo { + isAccountPeerOutcome = forwardInfo.author?.id == accountPeerId + } else { + isAccountPeerOutcome = message.author?.id == accountPeerId + } + let rawString: String - if message.author?.id == accountPeerId { + if isAccountPeerOutcome { if value == 1, let tonAmount = dice.tonAmount { let value = formatTonAmountText(tonAmount, dateTimeFormat: dateTimeFormat) rawString = strings.Conversation_EmojiStake_LostYou(value).string diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index a33742767d..49975963fd 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -115,6 +115,7 @@ swift_library( "//submodules/TelegramUI/Components/ComposePollScreen", "//submodules/AlertUI:AlertUI", "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TelegramUI/Components/OpenUserGeneratedUrl:OpenUserGeneratedUrl", "//submodules/TouchDownGesture:TouchDownGesture", "//submodules/SwipeToDismissGesture:SwipeToDismissGesture", "//submodules/DirectionalPanGesture:DirectionalPanGesture", @@ -137,7 +138,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 +154,6 @@ swift_library( "//submodules/ItemListPeerItem:ItemListPeerItem", "//submodules/ContactsPeerItem:ContactsPeerItem", "//submodules/TelegramPermissionsUI:TelegramPermissionsUI", - "//submodules/PeersNearbyIconNode:PeersNearbyIconNode", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/PasscodeUI:PasscodeUI", "//submodules/CallListUI:CallListUI", @@ -171,11 +170,9 @@ swift_library( "//submodules/YuvConversion:YuvConversion", "//submodules/JoinLinkPreviewUI:JoinLinkPreviewUI", "//submodules/LanguageLinkPreviewUI:LanguageLinkPreviewUI", - "//submodules/WebSearchUI:WebSearchUI", "//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/TimeSelectionActionSheet/BUILD b/submodules/TelegramUI/Components/AlertComponent/AlertHeaderComponent/BUILD similarity index 55% rename from submodules/TelegramUI/Components/TimeSelectionActionSheet/BUILD rename to submodules/TelegramUI/Components/AlertComponent/AlertHeaderComponent/BUILD index b4680d882d..bfe177cde4 100644 --- a/submodules/TelegramUI/Components/TimeSelectionActionSheet/BUILD +++ b/submodules/TelegramUI/Components/AlertComponent/AlertHeaderComponent/BUILD @@ -1,8 +1,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") swift_library( - name = "TimeSelectionActionSheet", - module_name = "TimeSelectionActionSheet", + name = "AlertHeaderComponent", + module_name = "AlertHeaderComponent", srcs = glob([ "Sources/**/*.swift", ]), @@ -10,14 +10,11 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/Display", "//submodules/AsyncDisplayKit", - "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/TelegramCore", + "//submodules/Display", + "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", - "//submodules/TelegramStringFormatting", - "//submodules/AccountContext", - "//submodules/UIKitRuntimeUtils", + "//submodules/TelegramUI/Components/AlertComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertHeaderComponent/Sources/AlertHeaderComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertHeaderComponent/Sources/AlertHeaderComponent.swift new file mode 100644 index 0000000000..31d3c1d9e7 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertHeaderComponent/Sources/AlertHeaderComponent.swift @@ -0,0 +1,64 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import TelegramPresentationData +import AlertComponent + +public final class AlertHeaderComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + let component: AnyComponentWithIdentity + + public init( + component: AnyComponentWithIdentity + ) { + self.component = component + } + + public static func ==(lhs: AlertHeaderComponent, rhs: AlertHeaderComponent) -> Bool { + if lhs.component != rhs.component { + return false + } + return true + } + + public final class View: UIView { + private let componentView = ComponentView() + + private var component: AlertHeaderComponent? + private weak var state: EmptyComponentState? + + func update(component: AlertHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let size: CGSize = CGSize(width: 80.0, height: 80.0) + + let componentSize = self.componentView.update( + transition: transition, + component: component.component.component, + environment: {}, + containerSize: size + ) + let frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - componentSize.width) / 2.0), y: 0.0), size: componentSize) + if let componentView = self.componentView.view { + if componentView.superview == nil { + self.addSubview(componentView) + } + transition.setFrame(view: componentView, frame: frame) + } + + return CGSize(width: availableSize.width, height: size.height + 11.0) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/BUILD b/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/BUILD new file mode 100644 index 0000000000..4bb3e82d62 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/BUILD @@ -0,0 +1,34 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AlertWebpagePreviewComponent", + module_name = "AlertWebpagePreviewComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/AccountContext", + "//submodules/WebsiteType", + "//submodules/UrlHandling", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/WallpaperPreviewMedia", + "//submodules/ChatPresentationInterfaceState", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/Sources/AlertWebpagePreviewComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/Sources/AlertWebpagePreviewComponent.swift new file mode 100644 index 0000000000..948f5eac49 --- /dev/null +++ b/submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent/Sources/AlertWebpagePreviewComponent.swift @@ -0,0 +1,590 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import TextFormat +import AccountContext +import WebsiteType +import UrlHandling +import AlertComponent +import ChatMessageAttachedContentNode +import ChatMessageBubbleContentNode +import ChatHistoryEntry +import ChatMessageItemCommon +import ChatControllerInteraction +import WallpaperPreviewMedia +import ChatPresentationInterfaceState + +private struct AlertWebpagePreviewContent { + var title: String? + var subtitle: NSAttributedString? + var text: String? + var entities: [MessageTextEntity]? + var mediaAndFlags: ([EngineRawMedia], ChatMessageAttachedContentNodeMediaFlags)? + var mediaBadge: String? +} + +public final class AlertWebpagePreviewComponent: Component { + public typealias EnvironmentType = AlertComponentEnvironment + + let context: AccountContext + let presentationData: PresentationData + let webpage: TelegramMediaWebpage + let peer: EnginePeer? + + public init( + context: AccountContext, + presentationData: PresentationData, + webpage: TelegramMediaWebpage, + peer: EnginePeer? = nil + ) { + self.context = context + self.presentationData = presentationData + self.webpage = webpage + self.peer = peer + } + + public static func ==(lhs: AlertWebpagePreviewComponent, rhs: AlertWebpagePreviewComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.presentationData != rhs.presentationData { + return false + } + if lhs.webpage != rhs.webpage { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + public final class View: UIView { + private let contentNode: ChatMessageAttachedContentNode + + private var component: AlertWebpagePreviewComponent? + private var controllerInteraction: ChatControllerInteraction? + private weak var controllerInteractionContext: AccountContext? + + override init(frame: CGRect) { + self.contentNode = ChatMessageAttachedContentNode() + self.contentNode.isUserInteractionEnabled = false + + super.init(frame: frame) + + self.addSubview(self.contentNode.view) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AlertWebpagePreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + guard case let .Loaded(webpageContent) = component.webpage.content else { + return CGSize(width: availableSize.width, height: 0.0) + } + + let environment = environment[AlertComponentEnvironment.self] + let presentationData = ChatPresentationData( + theme: ChatPresentationThemeData( + theme: environment.theme, + wallpaper: component.presentationData.chatWallpaper + ), + fontSize: component.presentationData.chatFontSize, + strings: environment.strings, + dateTimeFormat: component.presentationData.dateTimeFormat, + nameDisplayOrder: component.presentationData.nameDisplayOrder, + disableAnimations: true, + largeEmoji: component.presentationData.largeEmoji, + chatBubbleCorners: PresentationChatBubbleCorners( + mainRadius: component.presentationData.chatBubbleCorners.mainRadius, + auxiliaryRadius: component.presentationData.chatBubbleCorners.auxiliaryRadius, + mergeBubbleCorners: component.presentationData.chatBubbleCorners.mergeBubbleCorners, + hasTails: false + ), + animatedEmojiScale: 1.0, + isPreview: true + ) + + let controllerInteraction: ChatControllerInteraction + if let current = self.controllerInteraction, self.controllerInteractionContext === component.context { + controllerInteraction = current + } else { + controllerInteraction = ChatControllerInteraction( + openMessage: { _, _ in + return false + }, + openPeer: { _, _, _, _ in }, + openPeerMention: { _, _ in }, + openMessageContextMenu: { _, _, _, _, _, _ in }, + openMessageReactionContextMenu: { _, _, _, _ in }, + updateMessageReaction: { _, _, _, _ in }, + activateMessagePinch: { _ in }, + openMessageContextActions: { _, _, _, _ in }, + navigateToMessage: { _, _, _ in }, + navigateToMessageStandalone: { _ in }, + navigateToThreadMessage: { _, _, _ in }, + tapMessage: nil, + clickThroughMessage: { _, _ in }, + toggleMessagesSelection: { _, _ in }, + sendCurrentMessage: { _, _ in }, + sendMessage: { _, _ in }, + sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, + sendEmoji: { _, _, _ in }, + sendGif: { _, _, _, _, _ in return false }, + sendBotContextResultAsGif: { _, _, _, _, _, _ in + return false + }, + editGif: { _, _ in }, + requestMessageActionCallback: { _, _, _, _, _ in }, + requestMessageActionUrlAuth: { _, _ in }, + activateSwitchInline: { _, _, _ in }, + openUrl: { _ in }, + openExternalInstantPage: { _ in }, + shareCurrentLocation: { _ in }, + shareAccountContact: { _ in }, + sendBotCommand: { _, _ in }, + openInstantPage: { _, _ in }, + openWallpaper: { _ in }, + openTheme: { _ in }, + openHashtag: { _, _ in }, + updateInputState: { _ in }, + updateInputMode: { _ in }, + updatePresentationState: { _ in }, + openMessageShareMenu: { _ in }, + presentController: { _, _ in }, + presentControllerInCurrent: { _, _ in }, + navigationController: { + return nil + }, + chatControllerNode: { + return nil + }, + presentGlobalOverlayController: { _, _ in }, + callPeer: { _, _ in }, + openConferenceCall: { _ in }, + longTap: { _, _ in }, + todoItemLongTap: { _, _ in }, + pollOptionLongTap: { _, _ in }, + openCheckoutOrReceipt: { _, _ in }, + openSearch: {}, + setupReply: { _ in }, + canSetupReply: { _ in + return .none + }, + canSendMessages: { + return false + }, + navigateToFirstDateMessage: { _, _ in }, + requestRedeliveryOfFailedMessages: { _ in }, + addContact: { _ in }, + rateCall: { _, _, _ in }, + requestSelectMessagePollOptions: { _, _ in }, + requestAddMessagePollOption: { _, _, _, _, _ in }, + requestOpenMessagePollResults: { _, _ in }, + openAppStorePage: {}, + displayMessageTooltip: { _, _, _, _, _ in }, + seekToTimecode: { _, _, _ in }, + scheduleCurrentMessage: { _ in }, + sendScheduledMessagesNow: { _ in }, + editScheduledMessagesTime: { _ in }, + performTextSelectionAction: { _, _, _, _, _ in }, + displayImportedMessageTooltip: { _ in }, + displaySwipeToReplyHint: {}, + dismissReplyMarkupMessage: { _ in }, + openMessagePollResults: { _, _ in }, + openPollCreation: { _, _ in }, + openPollMedia: { _, _ in }, + displayPollSolution: { _, _ in }, + displayPsa: { _, _ in }, + displayDiceTooltip: { _ in }, + animateDiceSuccess: { _, _ in }, + displayPremiumStickerTooltip: { _, _ in }, + displayEmojiPackTooltip: { _, _ in }, + openPeerContextMenu: { _, _, _, _, _ in }, + openMessageReplies: { _, _, _ in }, + openReplyThreadOriginalMessage: { _ in }, + openMessageStats: { _ in }, + editMessageMedia: { _, _ in }, + copyText: { _ in }, + displayUndo: { _ in }, + isAnimatingMessage: { _ in + return false + }, + getMessageTransitionNode: { + return nil + }, + updateChoosingSticker: { _ in }, + commitEmojiInteraction: { _, _, _, _ in }, + openLargeEmojiInfo: { _, _, _ in }, + openJoinLink: { _ in }, + openWebView: { _, _, _, _ in }, + activateAdAction: { _, _, _, _ in }, + adContextAction: { _, _, _ in }, + removeAd: { _ in }, + openRequestedPeerSelection: { _, _, _, _ in }, + saveMediaToFiles: { _ in }, + openNoAdsDemo: {}, + openAdsInfo: {}, + displayGiveawayParticipationStatus: { _ in }, + openPremiumStatusInfo: { _, _, _, _ in }, + openRecommendedChannelContextMenu: { _, _, _ in }, + openGroupBoostInfo: { _, _ in }, + openStickerEditor: {}, + openAgeRestrictedMessageMedia: { _, _ in }, + playMessageEffect: { _ in }, + editMessageFactCheck: { _ in }, + sendGift: { _ in }, + openUniqueGift: { _ in }, + openMessageFeeException: {}, + requestMessageUpdate: { _, _, _ in }, + cancelInteractiveKeyboardGestures: {}, + dismissTextInput: {}, + scrollToMessageId: { _, _ in }, + scrollToMessageIdWithAnchor: { _, _ in }, + navigateToStory: { _, _ in }, + attemptedNavigationToPrivateQuote: { _ in }, + forceUpdateWarpContents: {}, + playShakeAnimation: {}, + displayQuickShare: { _, _, _ in }, + updateChatLocationThread: { _, _ in }, + requestToggleTodoMessageItem: { _, _, _ in }, + displayTodoToggleUnavailable: { _ in }, + openStarsPurchase: { _ in }, + openRankInfo: { _, _, _ in }, + openSetPeerAvatar: {}, + displayPollRestrictedToast: { _ in }, + automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, + pollActionState: ChatInterfacePollActionState(), + stickerSettings: ChatInterfaceStickerSettings(), + presentationContext: ChatPresentationContext(context: component.context, backgroundNode: nil) + ) + self.controllerInteraction = controllerInteraction + self.controllerInteractionContext = component.context + } + + let author = TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: ._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) + let message = EngineRawMessage( + stableId: 0, + stableVersion: 0, + id: EngineMessage.Id(peerId: author.id, namespace: Namespaces.Message.Local, id: 0), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), + flags: [.Incoming], + tags: [], + globalTags: [], + localTags: [], + customTags: [], + forwardInfo: nil, + author: component.peer?._asPeer() ?? author, + text: "", + attributes: [], + media: [component.webpage], + peers: EngineSimpleDictionary(), + associatedMessages: EngineSimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + let associatedData = ChatMessageItemAssociatedData( + automaticDownloadPeerType: .contact, + automaticDownloadPeerId: nil, + automaticDownloadNetworkType: .cellular, + isRecentActions: false, + subject: nil, + contactsPeerIds: Set(), + animatedEmojiStickers: [:], + forcedResourceStatus: nil, + availableReactions: nil, + availableMessageEffects: nil, + savedMessageTags: nil, + defaultReaction: nil, + areStarReactionsEnabled: false, + isPremium: false, + accountPeer: nil, + forceInlineReactions: true, + isStandalone: true + ) + let previewContent = makeAlertWebpagePreviewContent( + context: component.context, + presentationData: presentationData, + automaticDownloadSettings: controllerInteraction.automaticMediaDownloadSettings, + associatedData: associatedData, + message: message, + webpage: webpageContent + ) + + let constrainedWidth = max(1.0, availableSize.width + 50.0) + let constrainedSize = CGSize(width: constrainedWidth, height: 10000.0) + let contentNodeLayout = self.contentNode.asyncLayout() + let position = ChatMessageBubbleRelativePosition.None(.None(.Incoming)) + + let (initialWidth, continueLayout) = contentNodeLayout( + presentationData, + controllerInteraction.automaticMediaDownloadSettings, + associatedData, + ChatMessageEntryAttributes(), + component.context, + controllerInteraction, + message, + true, + .peer(id: message.id.peerId), + previewContent.title, + nil, + previewContent.subtitle, + previewContent.text, + previewContent.entities, + previewContent.mediaAndFlags, + previewContent.mediaBadge, + nil, + nil, + true, + ChatMessageItemLayoutConstants.compact, + .linear(top: position, bottom: position), + constrainedSize, + controllerInteraction.presentationContext.animationCache, + controllerInteraction.presentationContext.animationRenderer + ) + let (refinedWidth, finalizeLayout) = continueLayout(constrainedSize, .linear(top: position, bottom: position)) + let boundingWidth = min(constrainedWidth, max(initialWidth, refinedWidth)) + let (contentSize, apply) = finalizeLayout(boundingWidth) + apply(.None, true, nil) + + let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -10.0), size: contentSize) + transition.setFrame(view: self.contentNode.view, frame: contentFrame) + self.contentNode.visibility = .visible(1.0, CGRect(origin: CGPoint(), size: contentSize)) + + return CGSize(width: availableSize.width, height: contentSize.height - 18.0) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func makeAlertWebpagePreviewContent( + context: AccountContext, + presentationData: ChatPresentationData, + automaticDownloadSettings: MediaAutoDownloadSettings, + associatedData: ChatMessageItemAssociatedData, + message: EngineRawMessage, + webpage: TelegramMediaWebpageLoadedContent +) -> AlertWebpagePreviewContent { + var result = AlertWebpagePreviewContent() + let type = websiteType(of: webpage.websiteName) + + if let websiteName = webpage.websiteName, !websiteName.isEmpty { + result.title = websiteName + } + + if let title = webpage.title, !title.isEmpty { + result.subtitle = NSAttributedString(string: title, font: Font.semibold(15.0)) + } + + if let textValue = webpage.text, !textValue.isEmpty { + result.text = textValue + var entityTypes: EnabledEntityTypes = [.allUrl] + switch type { + case .twitter, .instagram: + entityTypes.insert(.mention) + entityTypes.insert(.hashtag) + entityTypes.insert(.external) + default: + break + } + result.entities = generateTextEntities(textValue, enabledTypes: entityTypes) + } + + var mainMedia: EngineRawMedia? + var automaticPlayback = false + if let file = webpage.file, (file.isAnimated && context.sharedContext.energyUsageSettings.autoplayGif) || (!file.isAnimated && context.sharedContext.energyUsageSettings.autoplayVideo) { + if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) { + automaticPlayback = true + } else { + automaticPlayback = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.resource.id)) != nil + } + } + + switch type { + case .instagram, .twitter: + if automaticPlayback { + mainMedia = webpage.story ?? webpage.file ?? webpage.image + } else { + mainMedia = webpage.story ?? webpage.image ?? webpage.file + } + default: + mainMedia = webpage.story ?? webpage.file ?? webpage.image + } + + let themeMimeType = "application/x-tgtheme-ios" + + switch webpage.type { + case "telegram_background": + var colors: [UInt32] = [] + var rotation: Int32? + if let wallpaper = parseWallpaperUrl(sharedContext: context.sharedContext, url: webpage.url) { + if case let .color(color) = wallpaper { + colors = [color.rgb] + } else if case let .gradient(colorsValue, rotationValue) = wallpaper { + colors = colorsValue + rotation = rotationValue + } + } + + var content: WallpaperPreviewMediaContent? + if !colors.isEmpty { + if colors.count >= 2 { + content = .gradient(colors, rotation) + } else { + content = .color(UIColor(rgb: colors[0])) + } + } + if let content { + let media = WallpaperPreviewMedia(content: content) + result.mediaAndFlags = ([media], []) + } + case "telegram_theme": + var file: TelegramMediaFile? + var settings: TelegramThemeSettings? + var isSupported = false + + for attribute in webpage.attributes { + if case let .theme(attribute) = attribute { + if let attributeSettings = attribute.settings { + settings = attributeSettings + isSupported = true + } else if let filteredFile = attribute.files.filter({ $0.mimeType == themeMimeType }).first { + file = filteredFile + isSupported = true + } + } + } + + if !isSupported, let contentFile = webpage.file { + isSupported = true + file = contentFile + } + if let file { + let media = WallpaperPreviewMedia(content: .file(file: file, colors: [], rotation: nil, intensity: nil, true, isSupported)) + result.mediaAndFlags = ([media], []) + } else if let settings { + let media = WallpaperPreviewMedia(content: .themeSettings(settings)) + result.mediaAndFlags = ([media], []) + } + case "telegram_nft": + for attribute in webpage.attributes { + if case let .starGift(gift) = attribute, case let .unique(uniqueGift) = gift.gift { + let media = UniqueGiftPreviewMedia(content: uniqueGift) + result.mediaAndFlags = ([media], []) + break + } + } + case "telegram_auction": + for attribute in webpage.attributes { + if case let .giftAuction(giftAuction) = attribute, case let .generic(gift) = giftAuction.gift { + let media = GiftAuctionPreviewMedia(content: gift, endTime: giftAuction.endDate) + result.mediaAndFlags = ([media], []) + break + } + } + default: + if var file = mainMedia as? TelegramMediaFile, webpage.type != "telegram_theme" { + if webpage.imageIsVideoCover, let image = webpage.image { + file = file.withUpdatedVideoCover(image) + } + + if let embedUrl = webpage.embedUrl, !embedUrl.isEmpty { + if automaticPlayback { + result.mediaAndFlags = ([file], [.preferMediaBeforeText]) + } else { + result.mediaAndFlags = ([webpage.image ?? file], [.preferMediaBeforeText]) + } + } else if webpage.type == "telegram_background" { + var colors: [UInt32] = [] + var rotation: Int32? + var intensity: Int32? + if let wallpaper = parseWallpaperUrl(sharedContext: context.sharedContext, url: webpage.url), case let .slug(_, _, colorsValue, intensityValue, rotationValue) = wallpaper { + colors = colorsValue + rotation = rotationValue + intensity = intensityValue + } + let media = WallpaperPreviewMedia(content: .file(file: file, colors: colors, rotation: rotation, intensity: intensity, false, false)) + result.mediaAndFlags = ([media], [.preferMediaAspectFilled]) + if let fileSize = file.size { + result.mediaBadge = dataSizeString(fileSize, formatting: DataSizeStringFormatting(chatPresentationData: presentationData)) + } + } else { + result.mediaAndFlags = ([file], []) + } + } else if let image = mainMedia as? TelegramMediaImage { + if let type = webpage.type, ["photo", "video", "embed", "gif", "document", "telegram_album"].contains(type) { + var flags = ChatMessageAttachedContentNodeMediaFlags() + if webpage.instantPage != nil, let largest = largestImageRepresentation(image.representations) { + if largest.dimensions.width >= 256 { + flags.insert(.preferMediaBeforeText) + } + } else if let embedUrl = webpage.embedUrl, !embedUrl.isEmpty { + flags.insert(.preferMediaBeforeText) + } + result.mediaAndFlags = ([image], flags) + } else if largestImageRepresentation(image.representations)?.dimensions != nil { + result.mediaAndFlags = ([image], []) + } + } else if let story = mainMedia as? TelegramMediaStory { + result.mediaAndFlags = ([story], [.preferMediaBeforeText, .titleBeforeMedia]) + } + } + + switch webpage.type { + case "telegram_background": + result.title = presentationData.strings.Conversation_ChatBackground + result.subtitle = nil + result.text = nil + result.entities = nil + case "telegram_theme": + result.title = presentationData.strings.Conversation_Theme + result.text = nil + result.entities = nil + case "telegram_nft", "telegram_auction": + result.text = nil + result.entities = nil + default: + break + } + + for attribute in webpage.attributes { + if case let .stickerPack(stickerPack) = attribute, !stickerPack.files.isEmpty { + result.mediaAndFlags = (stickerPack.files, [.preferMediaInline, .stickerPack]) + break + } else if case let .giftCollection(giftCollection) = attribute, !giftCollection.files.isEmpty { + result.mediaAndFlags = (giftCollection.files, [.preferMediaInline, .stickerPack]) + break + } + } + + if defaultWebpageImageSizeIsSmall(webpage: webpage) { + result.mediaAndFlags?.1.insert(.preferMediaInline) + } + + if let isMediaLargeByDefault = webpage.isMediaLargeByDefault, !isMediaLargeByDefault { + result.mediaAndFlags?.1.insert(.preferMediaInline) + } + + return result +} diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift index 900b1c44c4..2cc5f0ef27 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -381,13 +381,30 @@ private final class AlertScreenComponent: Component { if itemView.superview == nil { self.backgroundView.contentView.addSubview(itemView) item.parentState = state + + transition.animateAlpha(view: itemView, from: 0.0, to: 1.0) } - transition.setFrame(view: itemView, frame: itemFrame) + itemTransition.setFrame(view: itemView, frame: itemFrame) } contentOriginY += itemSize.height } } + var removeKeys: [AnyHashable] = [] + for key in self.contentItems.keys { + if !validContentIds.contains(key) { + removeKeys.append(key) + if let itemView = self.contentItems[key]?.view { + transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + itemView.removeFromSuperview() + }) + } + } + } + for key in removeKeys { + self.contentItems.removeValue(forKey: key) + } + if !contentOriginY.isZero { alertHeight += contentOriginY alertHeight += contentBottomInset @@ -407,9 +424,9 @@ private final class AlertScreenComponent: Component { font: .bold ) let destructiveActionTheme = AlertActionComponent.Theme( - background: environment.theme.list.itemDestructiveColor, - foreground: .white, - secondary: .white.withMultipliedAlpha(0.6), + background: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), + foreground: environment.theme.list.itemDestructiveColor, + secondary: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.6), font: .regular ) let defaultDestructiveActionTheme = AlertActionComponent.Theme( diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift index 4f2312a393..231d46cbbe 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertContent.swift @@ -323,23 +323,23 @@ public final class AlertTextComponent: Component { } var size = CGSize(width: availableSize.width, height: textSize.height) if case .background = component.style { - let backgroundSize = CGSize(width: availableSize.width + 20.0, height: textSize.height + backgroundInset * 2.0) + let backgroundSize = CGSize(width: availableSize.width + 20.0 + component.insets.left + component.insets.right, height: textSize.height + backgroundInset * 2.0) size = backgroundSize - textOffset = CGPoint(x: textOffset.x, y: backgroundInset) + textOffset = CGPoint(x: textOffset.x, y: backgroundInset + 1.0) let _ = self.background.update( transition: transition, component: AnyComponent( FilledRoundedRectangleComponent( color: textColor.withMultipliedAlpha(0.1), - cornerRadius: .value(10.0), + cornerRadius: .value(12.0), smoothCorners: true ) ), environment: {}, containerSize: backgroundSize ) - let backgroundFrame = CGRect(origin: CGPoint(x: -10.0, y: component.insets.top), size: backgroundSize) + let backgroundFrame = CGRect(origin: CGPoint(x: -10.0 - component.insets.left, y: component.insets.top), size: backgroundSize) if let backgroundView = self.background.view { if backgroundView.superview == nil { self.addSubview(backgroundView) diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift index 02494198e8..23f1203c65 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift @@ -836,7 +836,7 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon insets.bottom += 60.0 let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { insets.left += inset insets.right += inset } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 1dde1fc55d..e9e7be9e0c 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -1539,6 +1539,7 @@ final class AvatarEditorScreenComponent: Component { videoVolume: nil, additionalVideoPath: nil, additionalVideoIsDual: false, + additionalVideoMirroringChanges: [], additionalVideoPosition: nil, additionalVideoScale: nil, additionalVideoRotation: nil, diff --git a/submodules/TelegramUI/Components/BottomButtonPanelComponent/BUILD b/submodules/TelegramUI/Components/BottomButtonPanelComponent/BUILD index 43b5271ce3..37977a29be 100644 --- a/submodules/TelegramUI/Components/BottomButtonPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/BottomButtonPanelComponent/BUILD @@ -16,7 +16,9 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/Components/ComponentDisplayAdapters", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/EdgeEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift b/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift index 966c9230fd..97a11abe6c 100644 --- a/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift +++ b/submodules/TelegramUI/Components/BottomButtonPanelComponent/Sources/BottomButtonPanelComponent.swift @@ -5,7 +5,9 @@ import AsyncDisplayKit import ComponentFlow import ComponentDisplayAdapters import TelegramPresentationData +import AnimatedTextComponent import ButtonComponent +import EdgeEffect import MultilineTextComponent public final class BottomButtonPanelComponent: Component { @@ -58,20 +60,18 @@ public final class BottomButtonPanelComponent: Component { } public class View: UIView { - private let backgroundView: BlurredBackgroundView - private let separatorLayer: SimpleLayer + private let edgeEffectView: EdgeEffectView private let actionButton = ComponentView() - + private var component: BottomButtonPanelComponent? override public init(frame: CGRect) { - self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) - self.separatorLayer = SimpleLayer() + self.edgeEffectView = EdgeEffectView() + self.edgeEffectView.isUserInteractionEnabled = false super.init(frame: frame) - self.addSubview(self.backgroundView) - self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.edgeEffectView) } required public init?(coder: NSCoder) { @@ -79,7 +79,6 @@ public final class BottomButtonPanelComponent: Component { } func update(component: BottomButtonPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let themeUpdated = self.component?.theme !== component.theme self.component = component let topInset: CGFloat = 8.0 @@ -91,28 +90,34 @@ public final class BottomButtonPanelComponent: Component { bottomInset = component.insets.bottom + 10.0 } - let height: CGFloat = topInset + 50.0 + bottomInset + let buttonHeight: CGFloat = 52.0 + let height: CGFloat = topInset + buttonHeight + bottomInset + + let edgeEffectFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)) + transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) + self.edgeEffectView.update(content: component.theme.list.blocksBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: transition) - if themeUpdated { - self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor - } - - - let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height)) - transition.setFrame(view: self.backgroundView, frame: backgroundFrame) - self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition) - - transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - var buttonTitleVStack: [AnyComponentWithIdentity] = [] - - let titleString = NSMutableAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) - buttonTitleVStack.append(AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(titleString))))) + buttonTitleVStack.append(AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 18.0, weight: .semibold, traits: .monospacedNumbers), + color: component.theme.list.itemCheckColors.foregroundColor, + items: [ + AnimatedTextComponent.Item(id: AnyHashable("title"), content: .text(component.title)) + ], + noDelay: true, + blur: false + )))) if let label = component.label { - let labelString = NSMutableAttributedString(string: label, font: Font.semibold(11.0), textColor: component.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center) - buttonTitleVStack.append(AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(labelString))))) + buttonTitleVStack.append(AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 11.0, weight: .semibold, traits: .monospacedNumbers), + color: component.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), + items: [ + AnimatedTextComponent.Item(id: AnyHashable("label"), content: .text(label)) + ], + noDelay: true, + blur: false + )))) } var buttonTitleContent: AnyComponent = AnyComponent(VStack(buttonTitleVStack, spacing: 1.0)) @@ -127,10 +132,10 @@ public final class BottomButtonPanelComponent: Component { 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), - cornerRadius: 10.0 + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: AnyComponentWithIdentity( id: 0, @@ -146,7 +151,7 @@ public final class BottomButtonPanelComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: 50.0) + containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: buttonHeight) ) if let actionButtonView = self.actionButton.view { if actionButtonView.superview == nil { diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 2b28a3c333..b62e67d0e4 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -344,13 +344,31 @@ public final class ButtonComponent: Component { case actualGlass case legacy } - + + public struct Gradient: Equatable { + public enum Animation: Equatable { + case horizontalShift(duration: Double) + } + + public var colors: [UIColor] + public var animation: Animation + + public init( + colors: [UIColor], + animation: Animation = .horizontalShift(duration: 4.5) + ) { + self.colors = colors + self.animation = animation + } + } + public var style: Style public var color: UIColor public var foreground: UIColor public var pressedColor: UIColor public var cornerRadius: CGFloat public var isShimmering: Bool + public var gradient: Gradient? public init( style: Style = .legacy, @@ -358,7 +376,8 @@ public final class ButtonComponent: Component { foreground: UIColor, pressedColor: UIColor, cornerRadius: CGFloat = 10.0, - isShimmering: Bool = false + isShimmering: Bool = false, + gradient: Gradient? = nil ) { self.style = style self.color = color @@ -366,6 +385,7 @@ public final class ButtonComponent: Component { self.pressedColor = pressedColor self.cornerRadius = cornerRadius self.isShimmering = isShimmering + self.gradient = gradient } public func withIsShimmering(_ isShimmering: Bool) -> Background { @@ -375,7 +395,8 @@ public final class ButtonComponent: Component { foreground: self.foreground, pressedColor: self.pressedColor, cornerRadius: self.cornerRadius, - isShimmering: isShimmering + isShimmering: isShimmering, + gradient: self.gradient ) } } @@ -468,6 +489,7 @@ public final class ButtonComponent: Component { private let glassHighlightRecognizer: GlassHighlightGestureRecognizer private var shimmeringView: ButtonShimmeringView? + private var gradientBackgroundView: AnimatedGradientBackgroundView? private var chromeView: UIImageView? private var contentItem: ContentItem? @@ -584,11 +606,49 @@ public final class ButtonComponent: Component { } transition.setFrame(view: glassHighlightContainerView, frame: CGRect(origin: .zero, size: size)) transition.setCornerRadius(layer: glassHighlightContainerView.layer, cornerRadius: cornerRadius) - + self.glassHighlightRecognizer.highlightContainerView = glassHighlightContainerView self.glassHighlightRecognizer.isEnabled = component.isEnabled && !component.displaysProgress } - + + private func updateGradientBackground(component: ButtonComponent, contentContainerView: UIView, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { + guard component.background.style != .actualGlass, let gradient = component.background.gradient, gradient.colors.count > 1 else { + if let gradientBackgroundView = self.gradientBackgroundView { + self.gradientBackgroundView = nil + if transition.animation.isImmediate { + gradientBackgroundView.removeFromSuperview() + } else { + gradientBackgroundView.layer.animateAlpha(from: gradientBackgroundView.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak gradientBackgroundView] _ in + gradientBackgroundView?.removeFromSuperview() + }) + } + } + return + } + + let gradientBackgroundView: AnimatedGradientBackgroundView + var gradientTransition = transition + if let current = self.gradientBackgroundView { + gradientBackgroundView = current + } else { + gradientTransition = .immediate + gradientBackgroundView = AnimatedGradientBackgroundView(frame: .zero) + gradientBackgroundView.alpha = 0.0 + self.gradientBackgroundView = gradientBackgroundView + } + + if gradientBackgroundView.superview !== contentContainerView { + gradientBackgroundView.removeFromSuperview() + contentContainerView.insertSubview(gradientBackgroundView, at: 0) + } else { + contentContainerView.sendSubviewToBack(gradientBackgroundView) + } + + gradientBackgroundView.update(size: size, gradient: gradient, cornerRadius: cornerRadius, transition: gradientTransition) + gradientTransition.setFrame(view: gradientBackgroundView, frame: CGRect(origin: .zero, size: size)) + transition.setAlpha(view: gradientBackgroundView, alpha: 1.0) + } + @objc private func pressed() { guard let component = self.component else { return @@ -694,6 +754,8 @@ public final class ButtonComponent: Component { transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: cornerRadius) } + self.updateGradientBackground(component: component, contentContainerView: contentContainerView, size: size, cornerRadius: cornerRadius, transition: transition) + if component.background.style == .glass, component.background.color.alpha > 1.0 - .ulpOfOne { self.updateGlassEffect(component: component, size: size, cornerRadius: cornerRadius, transition: transition) } else { @@ -773,7 +835,11 @@ public final class ButtonComponent: Component { shimmeringTransition = .immediate shimmeringView = ButtonShimmeringView(frame: .zero) self.shimmeringView = shimmeringView - contentContainerView.insertSubview(shimmeringView, at: 0) + if let gradientBackgroundView = self.gradientBackgroundView, gradientBackgroundView.superview === contentContainerView { + contentContainerView.insertSubview(shimmeringView, aboveSubview: gradientBackgroundView) + } else { + contentContainerView.insertSubview(shimmeringView, at: 0) + } } shimmeringView.update(size: size, background: component.background, cornerRadius: cornerRadius, transition: shimmeringTransition) shimmeringTransition.setFrame(view: shimmeringView, frame: CGRect(origin: .zero, size: size)) @@ -795,13 +861,15 @@ public final class ButtonComponent: Component { self.chromeView = chromeView if let shimmeringView = self.shimmeringView { contentContainerView.insertSubview(chromeView, aboveSubview: shimmeringView) + } else if let gradientBackgroundView = self.gradientBackgroundView, gradientBackgroundView.superview === contentContainerView { + contentContainerView.insertSubview(chromeView, aboveSubview: gradientBackgroundView) } else { contentContainerView.insertSubview(chromeView, at: 0) } chromeView.layer.compositingFilter = "overlayBlendMode" chromeView.alpha = 0.8 - chromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 26.0 * 2.0, height: 26.0 * 2.0), isDark: component.background.color.lightness < 0.36, fillColor: .clear) + chromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: size.height, height: size.height), isDark: component.background.color.lightness < 0.36, fillColor: .clear) } chromeTransition.setFrame(view: chromeView, frame: CGRect(origin: .zero, size: size)) } else if let chromeView = self.chromeView { @@ -829,6 +897,113 @@ public final class ButtonComponent: Component { } } +private final class AnimatedGradientBackgroundView: UIView { + private let backgroundView: UIImageView + private var animationView: UIImageView? + + private var currentGradient: ButtonComponent.Background.Gradient? + private var currentImageHeight: CGFloat? + + override init(frame: CGRect) { + self.backgroundView = UIImageView() + self.backgroundView.clipsToBounds = true + + super.init(frame: frame) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .continuous + self.backgroundView.layer.cornerCurve = .continuous + } + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + func update(size: CGSize, gradient: ButtonComponent.Background.Gradient, cornerRadius: CGFloat, transition: ComponentTransition) { + let bounds = CGRect(origin: .zero, size: size) + + transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius) + transition.setCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius) + transition.setFrame(view: self.backgroundView, frame: bounds) + + let imageHeight = max(1.0, size.height) + if self.currentGradient != gradient || self.currentImageHeight != imageHeight || self.backgroundView.image == nil { + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(gradient.colors.count - 1) + for i in 0 ..< gradient.colors.count { + locations.append(delta * CGFloat(i)) + } + + let image = generateGradientImage(size: CGSize(width: 200.0, height: imageHeight), colors: gradient.colors, locations: locations, direction: .horizontal) + self.backgroundView.image = image + self.animationView?.image = image + self.animationView?.layer.removeAnimation(forKey: "movement") + + self.currentGradient = gradient + self.currentImageHeight = imageHeight + } + + let animationView: UIImageView + if let current = self.animationView { + animationView = current + } else { + animationView = UIImageView() + animationView.image = self.backgroundView.image + self.animationView = animationView + self.backgroundView.addSubview(animationView) + } + + animationView.bounds = CGRect(origin: .zero, size: CGSize(width: size.width * 2.4, height: size.height)) + if animationView.layer.animation(forKey: "movement") == nil { + animationView.center = CGPoint(x: animationView.bounds.width / 2.0 - animationView.bounds.width * 0.35, y: size.height / 2.0) + } + self.setupGradientAnimations(size: size, gradient: gradient) + } + + private func setupGradientAnimations(size: CGSize, gradient: ButtonComponent.Background.Gradient) { + guard let animationView = self.animationView else { + return + } + + let duration: Double + switch gradient.animation { + case let .horizontalShift(value): + duration = value + } + + if animationView.layer.animation(forKey: "movement") == nil { + let offset = (animationView.bounds.width - size.width) / 2.0 + let previousValue = animationView.center.x + var newValue: CGFloat = offset + if offset - previousValue < animationView.bounds.width * 0.25 { + newValue -= animationView.bounds.width * 0.35 + } + animationView.center = CGPoint(x: newValue, y: animationView.bounds.height / 2.0) + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "position.x") + animation.duration = duration + animation.fromValue = previousValue + animation.toValue = newValue + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + CATransaction.setCompletionBlock { [weak self] in + self?.setupGradientAnimations(size: size, gradient: gradient) + } + + animationView.layer.add(animation, forKey: "movement") + CATransaction.commit() + } + } +} + private class ButtonShimmeringView: UIView { private var shimmerView = ShimmerEffectForegroundView() private var borderView = UIView() @@ -872,7 +1047,11 @@ private class ButtonShimmeringView: UIView { compositingFilter = nil } - self.backgroundColor = background.color + if let gradient = background.gradient, gradient.colors.count > 1 { + self.backgroundColor = .clear + } else { + self.backgroundColor = background.color + } self.layer.cornerRadius = cornerRadius self.borderMaskView.layer.cornerRadius = cornerRadius diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 737457540a..9b09ee1d8c 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1378,7 +1378,7 @@ private final class CameraScreenComponent: CombinedComponent { let previewHeight = floorToScreenPixels(availableSize.width * 1.77778) if !isTablet { if availableSize.height < previewHeight + 30.0 { - controlsBottomInset = -48.0 + controlsBottomInset = -70.0 } } @@ -1505,6 +1505,7 @@ private final class CameraScreenComponent: CombinedComponent { hasAppeared: component.hasAppeared && hasAllRequiredAccess, hasAccess: hasAllRequiredAccess, hideControls: component.cameraState.collageProgress > 1.0 - .ulpOfOne, + controlsBottomInset: controlsBottomInset, collageProgress: component.cameraState.collageProgress, collageCount: component.cameraState.isCollageEnabled ? component.cameraState.collageGrid.count : nil, tintColor: controlsTintColor, @@ -2111,7 +2112,7 @@ private final class CameraScreenComponent: CombinedComponent { if isTablet { availableModeControlSize = CGSize(width: floor(panelWidth), height: 120.0) } else { - availableModeControlSize = availableSize + availableModeControlSize = CGSize(width: availableSize.width - 140.0, height: availableSize.height) } var availableModes: [CameraState.CameraMode] = [.photo, .video] 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/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index 1fecc3d492..eadde87f2c 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -310,7 +310,7 @@ private final class ShutterButtonContentComponent: Component { glassAlpha = 0.0 chromeAlpha = 0.65 labelAlpha = progress ? 0.0 : 1.0 - chromeSize = CGSize(width: 326.0, height: 53.0 - UIScreenPixel) + chromeSize = CGSize(width: 297.0, height: 53.0 - UIScreenPixel) hasProgress = progress } @@ -683,6 +683,7 @@ final class CaptureControlsComponent: Component { let hasAppeared: Bool let hasAccess: Bool let hideControls: Bool + let controlsBottomInset: CGFloat let collageProgress: Float let collageCount: Int? let tintColor: UIColor @@ -712,6 +713,7 @@ final class CaptureControlsComponent: Component { hasAppeared: Bool, hasAccess: Bool, hideControls: Bool, + controlsBottomInset: CGFloat, collageProgress: Float, collageCount: Int?, tintColor: UIColor, @@ -740,6 +742,7 @@ final class CaptureControlsComponent: Component { self.hasAppeared = hasAppeared self.hasAccess = hasAccess self.hideControls = hideControls + self.controlsBottomInset = controlsBottomInset self.collageProgress = collageProgress self.collageCount = collageCount self.tintColor = tintColor @@ -783,6 +786,9 @@ final class CaptureControlsComponent: Component { if lhs.hideControls != rhs.hideControls { return false } + if lhs.controlsBottomInset != rhs.controlsBottomInset { + return false + } if lhs.collageProgress != rhs.collageProgress { return false } @@ -1223,6 +1229,13 @@ final class CaptureControlsComponent: Component { let hideControls = component.hideControls + var bottomButtonSideInset: CGFloat = 16.0 + var bottomButtonTopInset: CGFloat = 21.0 + if component.controlsBottomInset < 0.0 { + bottomButtonSideInset = 9.0 + bottomButtonTopInset = -2.0 - UIScreenPixel + } + let galleryButtonFrame: CGRect let lockReferenceFrame: CGRect let gallerySize: CGSize @@ -1268,7 +1281,7 @@ final class CaptureControlsComponent: Component { galleryButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - galleryButtonSize.width) / 2.0), y: size.height - galleryButtonSize.height - 56.0), size: galleryButtonSize) lockReferenceFrame = .zero } else { - galleryButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: size.height + 21.0), size: galleryButtonSize) + galleryButtonFrame = CGRect(origin: CGPoint(x: bottomButtonSideInset, y: size.height + bottomButtonTopInset), size: galleryButtonSize) lockReferenceFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: floorToScreenPixels((size.height - galleryButtonSize.height) / 2.0)), size: galleryButtonSize) } if let galleryButtonView = self.galleryButtonView.view as? CameraButton.View { @@ -1360,7 +1373,7 @@ final class CaptureControlsComponent: Component { environment: {}, containerSize: availableSize ) - let bottomFlipButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - bottomFlipButtonSize.width - 16.0, y: 21.0), size: bottomFlipButtonSize) + let bottomFlipButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - bottomFlipButtonSize.width - bottomButtonSideInset, y: bottomButtonTopInset), size: bottomFlipButtonSize) if let bottomFlipButtonView = self.bottomFlipButton.view { if bottomFlipButtonView.superview == nil { self.bottomContainerView.contentView.addSubview(bottomFlipButtonView) @@ -1399,13 +1412,13 @@ final class CaptureControlsComponent: Component { environment: {}, containerSize: availableSize ) - let bottomFlipButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 21.0), size: bottomSettingsButtonSize) + let bottomSettingsButtonFrame = CGRect(origin: CGPoint(x: bottomButtonSideInset, y: bottomButtonTopInset), size: bottomSettingsButtonSize) if let bottomSettingsButtonView = self.bottomSettingsButton.view { if bottomSettingsButtonView.superview == nil { self.bottomContainerView.contentView.addSubview(bottomSettingsButtonView) } - transition.setBounds(view: bottomSettingsButtonView, bounds: CGRect(origin: .zero, size: bottomFlipButtonFrame.size)) - transition.setPosition(view: bottomSettingsButtonView, position: bottomFlipButtonFrame.center) + transition.setBounds(view: bottomSettingsButtonView, bounds: CGRect(origin: .zero, size: bottomSettingsButtonFrame.size)) + transition.setPosition(view: bottomSettingsButtonView, position: bottomSettingsButtonFrame.center) transition.setScale(view: bottomSettingsButtonView, scale: !isLiveStream || isLiveActive || isRecording || isTransitioning || hideControls ? 0.01 : 1.0) transition.setAlpha(view: bottomSettingsButtonView, alpha: !isLiveStream || isLiveActive || isRecording || isTransitioning || hideControls ? 0.0 : 1.0) @@ -1461,7 +1474,7 @@ final class CaptureControlsComponent: Component { let shutterButtonFrame = CGRect(origin: CGPoint(x: (availableSize.width - shutterButtonSize.width) / 2.0, y: (size.height - shutterButtonSize.height) / 2.0), size: shutterButtonSize) let guideSpacing: CGFloat = 9.0 - let guideSize = CGSize(width: isHolding ? component.isTablet ? 84.0 : 60.0 : 0.0, height: 1.0 + UIScreenPixel) + let guideSize = CGSize(width: isHolding ? (component.isTablet ? 84.0 : 60.0) : 0.0, height: 1.0 + UIScreenPixel) let guideAlpha: CGFloat = isHolding ? 1.0 : 0.0 let leftGuideFrame = CGRect(origin: CGPoint(x: shutterButtonFrame.minX - guideSpacing - guideSize.width, y: floorToScreenPixels((size.height - guideSize.height) / 2.0)), size: guideSize) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index fc56774d12..2bfcd3b081 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -70,7 +70,20 @@ final class ModeComponent: Component { return true } - final class View: UIView, ComponentTaggedView { + final class View: UIView, ComponentTaggedView, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + private struct LayoutData { + var containerSize: CGSize + var selectedFrame: CGRect + var cornerRadius: CGFloat? + var isTablet: Bool + } + private var component: ModeComponent? private var state: EmptyComponentState? @@ -107,6 +120,10 @@ final class ModeComponent: Component { private var backgroundContainer = GlassBackgroundContainerView() private var liquidLensView: LiquidLensView? + private let scrollView = ScrollView() + private let selectedScrollView = UIView() + private var ignoreScrolling = false + private var layoutData: LayoutData? private var itemViews: [AnyHashable: ItemView] = [:] private var selectedItemViews: [AnyHashable: ItemView] = [:] @@ -132,6 +149,27 @@ final class ModeComponent: Component { self.layer.allowsGroupOpacity = true + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = false + self.scrollView.scrollsToTop = false + self.scrollView.clipsToBounds = true + self.scrollView.delegate = self + self.scrollView.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let self else { + return false + } + return self.scrollView.contentOffset.x > .ulpOfOne + } + + self.selectedScrollView.clipsToBounds = true + self.selectedScrollView.isUserInteractionEnabled = false + self.addSubview(self.backgroundView) self.backgroundView.addSubview(self.backgroundContainer) } @@ -159,13 +197,14 @@ final class ModeComponent: Component { return self.backgroundView.frame.contains(point) } - private func item(at point: CGPoint) -> AnyHashable? { + private func item(at point: CGPoint, in view: UIView) -> AnyHashable? { var closestItem: (AnyHashable, CGFloat)? for (id, itemView) in self.itemViews { - if itemView.frame.contains(point) { + let itemFrame = itemView.convert(itemView.bounds, to: view) + if itemFrame.contains(point) { return id } else { - let distance = abs(point.x - itemView.center.x) + let distance = abs(point.x - itemFrame.midX) if let closestItemValue = closestItem { if closestItemValue.1 > distance { closestItem = (id, distance) @@ -178,22 +217,46 @@ final class ModeComponent: Component { return closestItem?.0 } - @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { - guard let component = self.component, let liquidLensView = self.liquidLensView else { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { return } - let location = recognizer.location(in: liquidLensView.contentView) + self.updateScrolling(transition: .immediate) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === self.tabSelectionRecognizer && otherGestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } + if otherGestureRecognizer === self.tabSelectionRecognizer && gestureRecognizer === self.scrollView.panGestureRecognizer { + return true + } + return false + } + + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let component = self.component else { + return + } + let location = recognizer.location(in: self) switch recognizer.state { case .began: - if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { + if let itemId = self.item(at: location, in: self), let itemView = self.itemViews[itemId] { let startX = itemView.frame.minX - 4.0 self.selectionGestureState = (startX, startX, itemId) self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } case .changed: if var selectionGestureState = self.selectionGestureState { + let translation = recognizer.translation(in: self) + if !component.isTablet && self.scrollView.isScrollEnabled && abs(translation.x) > 6.0 && abs(translation.x) > abs(translation.y) { + self.selectionGestureState = nil + recognizer.state = .cancelled + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + return + } selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x - if let itemId = self.item(at: location) { + if let itemId = self.item(at: location, in: self) { selectionGestureState.itemId = itemId } self.selectionGestureState = selectionGestureState @@ -214,8 +277,38 @@ final class ModeComponent: Component { break } } + + private func updateScrolling(transition: ComponentTransition) { + guard let component = self.component, let liquidLensView = self.liquidLensView, let layoutData = self.layoutData else { + return + } + + let contentOffsetX = layoutData.isTablet ? 0.0 : self.scrollView.bounds.minX + var lensSelection = (origin: layoutData.selectedFrame.origin, size: layoutData.selectedFrame.size) + if let selectionGestureState = self.selectionGestureState, !layoutData.isTablet { + lensSelection.origin = CGPoint(x: selectionGestureState.currentX, y: 0.0) + } + + if layoutData.isTablet { + lensSelection.size.width = layoutData.containerSize.width + } else { + lensSelection.origin.x -= contentOffsetX + lensSelection.origin.y = 0.0 + lensSelection.size.height = layoutData.containerSize.height + } + + let maxSelectionOriginX = max(0.0, layoutData.containerSize.width - lensSelection.size.width) + transition.setFrame(view: self.selectedScrollView, frame: CGRect(origin: .zero, size: layoutData.containerSize)) + transition.setBounds(view: self.selectedScrollView, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: layoutData.containerSize)) + + liquidLensView.update(size: layoutData.containerSize, cornerRadius: layoutData.cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, maxSelectionOriginX)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !layoutData.isTablet, isCollapsed: false, transition: transition) + self.backgroundContainer.update(size: layoutData.containerSize, isDark: true, transition: .immediate) + + self.scrollView.isScrollEnabled = !component.isTablet && self.scrollView.contentSize.width > self.scrollView.bounds.width + .ulpOfOne + } func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component self.component = component self.state = state @@ -228,16 +321,36 @@ final class ModeComponent: Component { liquidLensView = LiquidLensView(kind: isTablet ? .noContainer : .externalContainer) self.liquidLensView = liquidLensView self.backgroundContainer.contentView.addSubview(liquidLensView) + liquidLensView.contentView.addSubview(self.scrollView) + liquidLensView.selectedContentView.addSubview(self.selectedScrollView) let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) + tabSelectionRecognizer.delegate = self + tabSelectionRecognizer.cancelsTouchesInView = false self.tabSelectionRecognizer = tabSelectionRecognizer liquidLensView.addGestureRecognizer(tabSelectionRecognizer) } + if self.scrollView.superview == nil { + liquidLensView.contentView.addSubview(self.scrollView) + } + if self.selectedScrollView.superview == nil { + liquidLensView.selectedContentView.addSubview(self.selectedScrollView) + } self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11) - let inset: CGFloat = 23.0 - let spacing: CGFloat = isTablet ? 9.0 : 40.0 + var inset: CGFloat = 23.0 + let spacing: CGFloat + if isTablet { + spacing = 9.0 + } else { + if availableSize.width < 200.0 { + inset = 20.0 + spacing = 24.0 + } else { + spacing = 40.0 + } + } var i = 0 var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize) @@ -257,12 +370,16 @@ final class ModeComponent: Component { itemView = ItemView() itemView.isUserInteractionEnabled = false self.itemViews[id] = itemView - liquidLensView.contentView.addSubview(itemView) selectedItemView = ItemView() selectedItemView.isUserInteractionEnabled = false self.selectedItemViews[id] = selectedItemView - liquidLensView.selectedContentView.addSubview(selectedItemView) + } + if itemView.superview !== self.scrollView { + self.scrollView.addSubview(itemView) + } + if selectedItemView.superview !== self.selectedScrollView { + self.selectedScrollView.addSubview(selectedItemView) } let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor) @@ -297,47 +414,68 @@ final class ModeComponent: Component { transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in itemView.removeFromSuperview() }) + + if let selectedItemView = self.selectedItemViews[id] { + transition.setAlpha(view: selectedItemView, alpha: 0.0, completion: { _ in + selectedItemView.removeFromSuperview() + }) + } } } for id in removeKeys { self.itemViews.removeValue(forKey: id) + self.selectedItemViews.removeValue(forKey: id) } let totalSize: CGSize let size: CGSize + let contentSize: CGSize var cornerRadius: CGFloat? if isTablet { totalSize = CGSize(width: availableSize.width, height: tabletButtonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1)) size = CGSize(width: availableSize.width, height: availableSize.height) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: totalSize)) + contentSize = totalSize cornerRadius = 20.0 } else { size = CGSize(width: availableSize.width, height: buttonSize.height) totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalSize.width) / 2.0), y: 0.0), size: totalSize)) + let visibleSize = CGSize(width: min(availableSize.width, totalSize.width), height: totalSize.height) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - visibleSize.width) / 2.0), y: 0.0), size: visibleSize)) + contentSize = totalSize } let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size) transition.setFrame(view: self.backgroundContainer, frame: containerFrame) - - let selectionFrame = selectedFrame.insetBy(dx: -23.0, dy: 3.0) - var lensSelection: (origin: CGPoint, size: CGSize) - if let selectionGestureState = self.selectionGestureState, !isTablet { - lensSelection = (CGPoint(x: selectionGestureState.currentX, y: 0.0), selectionFrame.size) - } else { - lensSelection = (CGPoint(x: selectionFrame.minX, y: selectionFrame.minY), selectionFrame.size) - } - - if isTablet { - lensSelection.size.width = size.width - } else { - lensSelection.size.height = containerFrame.size.height - lensSelection.origin.y = 0.0 - } - transition.setFrame(view: liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size)) - liquidLensView.update(size: containerFrame.size, cornerRadius: cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, containerFrame.size.width - lensSelection.size.width)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !isTablet, isCollapsed: false, transition: transition) - self.backgroundContainer.update(size: containerFrame.size, isDark: true, transition: .immediate) + + let scrollViewFrame = CGRect(origin: .zero, size: containerFrame.size) + transition.setFrame(view: self.scrollView, frame: scrollViewFrame) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + self.scrollView.isScrollEnabled = !isTablet && contentSize.width > scrollViewFrame.width + .ulpOfOne + + self.layoutData = LayoutData(containerSize: containerFrame.size, selectedFrame: selectedFrame.insetBy(dx: -inset, dy: 3.0), cornerRadius: cornerRadius, isTablet: isTablet) + + self.ignoreScrolling = true + var scrollViewBounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollViewFrame.size) + let maxContentOffsetX = max(0.0, contentSize.width - scrollViewFrame.width) + let shouldFocusOnSelectedItem = previousComponent?.currentMode != component.currentMode || previousComponent?.availableModes != component.availableModes || self.scrollView.bounds.size != scrollViewFrame.size + if self.scrollView.isScrollEnabled && shouldFocusOnSelectedItem { + let scrollLookahead = min(60.0, scrollViewBounds.width * 0.25) + if scrollViewBounds.minX + scrollViewBounds.width - scrollLookahead < selectedFrame.maxX { + scrollViewBounds.origin.x = selectedFrame.maxX - scrollViewBounds.width + scrollLookahead + } + if scrollViewBounds.minX > selectedFrame.minX - scrollLookahead { + scrollViewBounds.origin.x = selectedFrame.minX - scrollLookahead + } + } + scrollViewBounds.origin.x = max(0.0, min(scrollViewBounds.origin.x, maxContentOffsetX)) + transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) return size } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index ed799fd9db..72289e0be8 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -43,7 +43,7 @@ final class ShutterBlobView: UIView { case .generic, .video, .transientToFlip: return CGSize(width: 0.63, height: 0.63) case .live: - return CGSize(width: 3.4, height: 0.55) + return CGSize(width: 3.1, height: 0.55) case .transientToLock, .lock, .stopVideo: return CGSize(width: 0.275, height: 0.275) } diff --git a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift index f9b1e28894..9acaf362e6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatButtonKeyboardInputNode/Sources/ChatButtonKeyboardInputNode.swift @@ -247,6 +247,7 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode, UIScrollViewDeleg self.scrollNode.view.clipsToBounds = true self.scrollNode.cornerRadius = 30.0 self.scrollNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.scrollNode.view.scrollsToTop = false self.backgroundTintMaskView.clipsToBounds = true self.backgroundTintMaskView.layer.cornerRadius = 30.0 @@ -380,14 +381,14 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode, UIScrollViewDeleg var dismissIfOnce = false switch markupButton.action { case .text: - self.controllerInteraction.sendMessage(markupButton.title) + self.controllerInteraction.sendMessage(markupButton.title, self.message?.id) dismissIfOnce = true case let .url(url): self.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: true, progress: Promise())) case .requestMap: - self.controllerInteraction.shareCurrentLocation() + self.controllerInteraction.shareCurrentLocation(self.message?.id) case .requestPhone: - self.controllerInteraction.shareAccountContact() + self.controllerInteraction.shareAccountContact(self.message?.id) case .openWebApp: if let message = self.message { self.controllerInteraction.requestMessageActionCallback(message._asMessage(), nil, true, false, nil) @@ -428,7 +429,7 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode, UIScrollViewDeleg self.controllerInteraction.requestMessageActionUrlAuth(url, .message(id: message.id, buttonId: buttonId)) } case let .setupPoll(isQuiz): - self.controllerInteraction.openPollCreation(isQuiz) + self.controllerInteraction.openPollCreation(self.message?.id, isQuiz) case let .openUserProfile(peerId): let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index f2ed195c59..6b7d770bab 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -216,6 +216,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics, deviceMetrics) = self.layoutData, let presentationInterfaceState = self.presentationInterfaceState { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, deviceMetrics: deviceMetrics, force: true) } + var didJoin = false self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peer.id, hash: nil) |> afterDisposed { [weak self] in Queue.mainQueue().async { @@ -223,7 +224,19 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { strongSelf.isJoining = false } } - }).startStrict(error: { [weak self] error in + }).startStrict(next: { [weak self] result in + guard let strongSelf = self else { + return + } + switch result { + case .joined: + didJoin = true + case let .webView(webView): + if let controller = strongSelf.interfaceInteraction?.getNavigationController()?.viewControllers.last as? ViewController { + context.sharedContext.openJoinChatWebView(context: context, parentController: controller, updatedPresentationData: nil, webView: webView) + } + } + }, error: { [weak self] error in guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else { return } @@ -254,6 +267,9 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { guard let self else { return } + if !didJoin { + return + } Queue.mainQueue().after(0.5) { if let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer { var canEditRank = false diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 2033456e3f..79e59151fc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -925,6 +925,8 @@ public final class ChatInlineSearchResultsListComponent: Component { }, performActiveSessionAction: { _, _ in }, + performBotConnectionReviewAction: { _, _ in + }, openChatFolderUpdates: { }, hideChatFolderUpdates: { diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index 93b4efb11f..6313e641bb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -1114,6 +1114,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, super.init(frame: CGRect(), textContainer: self.displayInternal.textContainer, disableTiling: disableTiling) self.delegate = self + self.scrollsToTop = false if #available(iOS 18.0, *) { self.supportsAdaptiveImageGlyph = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 876d1866f3..eb34eb8b44 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -805,7 +805,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI private var currentInputParams: Params? private var currentApplyParams: ListViewItemApply? private var contentLayoutInsets = UIEdgeInsets() - + + private func isQuickReplyMessageInputCustomContents() -> Bool { + guard let item = self.item else { + return false + } + if case let .customChatContents(contents) = item.associatedData.subject, case .quickReplyMessageInput = contents.kind { + return true + } + return false + } + required public init(rotated: Bool) { self.mainContextSourceNode = ContextExtractedContentContainingNode() self.mainContainerNode = ContextControllerSourceNode() @@ -841,12 +851,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let item = strongSelf.item, item.controllerInteraction.focusedPollAddOptionMessageId != nil { return .none } - if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { - if case let .action(action) = action, !action.contextMenuOnLongPress { - return .none - } - } - if let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + + let resolveLongTapAction: (InternalBubbleTapAction) -> ContextControllerSourceNode.ShouldBegin = { action in switch action { case .action: return .none @@ -864,6 +870,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } } + + let isQuickReplyMessageInputCustomContents = strongSelf.isQuickReplyMessageInputCustomContents() + if isQuickReplyMessageInputCustomContents, let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + return resolveLongTapAction(action) + } + if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { + if case let .action(action) = action, !action.contextMenuOnLongPress { + return .none + } + } + if !isQuickReplyMessageInputCustomContents, let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + return resolveLongTapAction(action) + } return .default } @@ -4648,12 +4667,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if strongSelf.selectionNode != nil { return false } - if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { - if case .action = action { - return false - } - } - if let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + + let resolveLongTapAction: (InternalBubbleTapAction) -> Bool = { action in switch action { case .action, .optionalAction: return false @@ -4661,6 +4676,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return !openContextMenu.selectAll } } + + let isQuickReplyMessageInputCustomContents = strongSelf.isQuickReplyMessageInputCustomContents() + if isQuickReplyMessageInputCustomContents, let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + return resolveLongTapAction(action) + } + if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { + if case .action = action { + return false + } + } + if !isQuickReplyMessageInputCustomContents, let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { + return resolveLongTapAction(action) + } return true } containerNode.activated = { [weak strongSelf, weak containerNode] gesture, location in @@ -5432,7 +5460,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } } } - if item.message.timestamp < 10 { + if item.message.timestamp < 10 && !strongSelf.isQuickReplyMessageInputCustomContents() { hasMenuGesture = false } strongSelf.mainContainerNode.isGestureEnabled = hasMenuGesture @@ -7246,6 +7274,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return nil } + override public func getAuthorNameNode() -> ASDisplayNode? { + guard let item = self.item, item.content.firstMessage.guestChatAttribute != nil else { + return nil + } + return self.nameNode + } + public func getQuoteRect(quote: String, offset: Int?) -> CGRect? { for contentNode in self.contentNodes { if let contentNode = contentNode as? ChatMessageTextBubbleContentNode { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index da8399997d..8499f95c2d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -46,6 +46,19 @@ private func mediaMergeableStyle(_ media: EngineRawMedia) -> ChatMessageMerge { return .fullyMerged } +private func anonymousGroupAdminSignature(message: EngineRawMessage, effectiveAuthor: EngineRawPeer?) -> String? { + guard let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info else { + return nil + } + guard effectiveAuthor?.id == channel.id else { + return nil + } + guard let signature = message.authorSignatureAttribute?.signature, !signature.isEmpty else { + return nil + } + return signature +} + private func messagesShouldBeMerged(accountPeerId: EnginePeer.Id, _ lhs: EngineRawMessage, _ rhs: EngineRawMessage) -> ChatMessageMerge { var lhsEffectiveAuthor: EngineRawPeer? = lhs.author var rhsEffectiveAuthor: EngineRawPeer? = rhs.author @@ -111,6 +124,14 @@ private func messagesShouldBeMerged(accountPeerId: EnginePeer.Id, _ lhs: EngineR sameAuthor = false } + if sameAuthor { + let lhsAnonymousAdminSignature = anonymousGroupAdminSignature(message: lhs, effectiveAuthor: lhsEffectiveAuthor) + let rhsAnonymousAdminSignature = anonymousGroupAdminSignature(message: rhs, effectiveAuthor: rhsEffectiveAuthor) + if lhsAnonymousAdminSignature != rhsAnonymousAdminSignature && (lhsAnonymousAdminSignature != nil || rhsAnonymousAdminSignature != nil) { + sameAuthor = false + } + } + var lhsEffectiveTimestamp = lhs.timestamp var rhsEffectiveTimestamp = rhs.timestamp diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 828078334e..684c085925 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -811,7 +811,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { if let item = self.item { switch button.action { case .text: - item.controllerInteraction.sendMessage(button.title) + item.controllerInteraction.sendMessage(button.title, item.message.id) case let .url(url): var concealed = true if url.hasPrefix("tg://") { @@ -819,9 +819,9 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { } item.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: concealed, progress: progress)) case .requestMap: - item.controllerInteraction.shareCurrentLocation() + item.controllerInteraction.shareCurrentLocation(item.message.id) case .requestPhone: - item.controllerInteraction.shareAccountContact() + item.controllerInteraction.shareAccountContact(item.message.id) case .openWebApp: item.controllerInteraction.requestMessageActionCallback(item.message, nil, true, false, progress) case let .callback(requiresPassword, data): @@ -901,6 +901,10 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { open func getStatusNode() -> ASDisplayNode? { return nil } + + open func getAuthorNameNode() -> ASDisplayNode? { + return nil + } private var attachedAvatarNodeOffset: CGFloat = 0.0 private var attachedAvatarNodeIsHidden: Bool = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index da1944f34d..795171b893 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -485,6 +485,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { private let resultBarIconNode: ASImageNode private var mediaNode: TransformImageNode? private var mediaVideoIconNode: ASImageNode? + private var mediaWebpageIconNode: ASImageNode? + private var mediaWebpageOverlayNode: ASDisplayNode? private var stickerMediaLayer: InlineStickerItemLayer? private var mediaHidden = false private(set) var mediaFrame: CGRect? @@ -763,9 +765,73 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { let alpha: CGFloat = self.mediaHidden ? 0.0 : 1.0 self.mediaNode?.alpha = alpha self.mediaVideoIconNode?.alpha = alpha + self.mediaWebpageOverlayNode?.alpha = alpha + self.mediaWebpageIconNode?.alpha = alpha self.stickerMediaLayer?.opacity = Float(alpha) } + private func updateMediaWebpageOverlayNode(frame: CGRect) { + let mediaWebpageOverlayNode: ASDisplayNode + if let current = self.mediaWebpageOverlayNode { + mediaWebpageOverlayNode = current + } else { + let current = ASDisplayNode() + current.displaysAsynchronously = false + current.isUserInteractionEnabled = false + current.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) + current.cornerRadius = 10.0 + self.mediaWebpageOverlayNode = current + mediaWebpageOverlayNode = current + } + + if let mediaWebpageIconNode = self.mediaWebpageIconNode { + self.containerNode.insertSubnode(mediaWebpageOverlayNode, belowSubnode: mediaWebpageIconNode) + } else if let mediaNode = self.mediaNode { + self.containerNode.insertSubnode(mediaWebpageOverlayNode, aboveSubnode: mediaNode) + } else if mediaWebpageOverlayNode.supernode == nil { + self.containerNode.addSubnode(mediaWebpageOverlayNode) + } + mediaWebpageOverlayNode.frame = frame + mediaWebpageOverlayNode.alpha = self.mediaHidden ? 0.0 : 1.0 + } + + private func removeMediaWebpageOverlayNode() { + if let mediaWebpageOverlayNode = self.mediaWebpageOverlayNode { + mediaWebpageOverlayNode.removeFromSupernode() + self.mediaWebpageOverlayNode = nil + } + } + + private func updateMediaWebpageIconNode(frame: CGRect, tintColor: UIColor) { + let mediaWebpageIconNode: ASImageNode + if let current = self.mediaWebpageIconNode { + mediaWebpageIconNode = current + } else { + let current = ASImageNode() + current.displaysAsynchronously = false + self.mediaWebpageIconNode = current + mediaWebpageIconNode = current + } + + if let mediaWebpageOverlayNode = self.mediaWebpageOverlayNode { + self.containerNode.insertSubnode(mediaWebpageIconNode, aboveSubnode: mediaWebpageOverlayNode) + } else if mediaWebpageIconNode.supernode == nil { + self.containerNode.addSubnode(mediaWebpageIconNode) + } + mediaWebpageIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: tintColor) + let iconSize = mediaWebpageIconNode.image?.size ?? CGSize(width: 30.0, height: 30.0) + mediaWebpageIconNode.frame = CGRect(origin: CGPoint(x: frame.midX - iconSize.width * 0.5, y: frame.midY - iconSize.height * 0.5), size: iconSize) + mediaWebpageIconNode.isHidden = false + mediaWebpageIconNode.alpha = self.mediaHidden ? 0.0 : 1.0 + } + + private func removeMediaWebpageIconNode() { + if let mediaWebpageIconNode = self.mediaWebpageIconNode { + mediaWebpageIconNode.removeFromSupernode() + self.mediaWebpageIconNode = nil + } + } + func setMediaHidden(_ hidden: Bool) { if self.mediaHidden != hidden { self.mediaHidden = hidden @@ -1166,6 +1232,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { stickerLayer.isVisibleForAnimations = true node.mediaNode?.removeFromSupernode() node.mediaNode = nil + node.removeMediaWebpageOverlayNode() + node.removeMediaWebpageIconNode() } else { if let stickerMediaLayer = node.stickerMediaLayer { stickerMediaLayer.removeFromSuperlayer() @@ -1189,11 +1257,14 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { mediaNode.frame = mediaFrame let mediaReference = AnyMediaReference.message(message: MessageReference(message), media: media) + let mediaUpdated = previousMedia?.isEqual(to: media) != true var imageSize = ChatMessagePollOptionNode.mediaSize var isVideo = false + var mediaWebpageIconTintColor: UIColor? + var mediaWebpageHasImageThumbnail = false if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { imageSize = largest.dimensions.cgSize.aspectFilled(ChatMessagePollOptionNode.mediaSize) - if previousMedia?.isEqual(to: media) != true, let photoReference = mediaReference.concrete(TelegramMediaImage.self) { + if mediaUpdated, let photoReference = mediaReference.concrete(TelegramMediaImage.self) { mediaNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: photoReference)) updatedFetchSignal = messageMediaImageInteractiveFetched(context: context, message: message, image: image, resource: largest.resource, storeToDownloadsPeerId: nil) } @@ -1201,7 +1272,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { if let dimensions = file.dimensions { imageSize = dimensions.cgSize.aspectFilled(ChatMessagePollOptionNode.mediaSize) } - if let fileReference = mediaReference.concrete(TelegramMediaFile.self), previousMedia?.isEqual(to: media) != true { + if let fileReference = mediaReference.concrete(TelegramMediaFile.self), mediaUpdated { if file.mimeType.hasPrefix("image/") { mediaNode.setSignal(instantPageImageFile(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, fetched: true)) } else { @@ -1209,8 +1280,53 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } } isVideo = file.isVideo + } else if let webpage = media as? TelegramMediaWebpage { + let webpageReference = WebpageReference(webpage) + if case let .Loaded(content) = webpage.content, let image = content.image { + if let largest = largestImageRepresentation(image.representations) { + imageSize = largest.dimensions.cgSize.aspectFilled(ChatMessagePollOptionNode.mediaSize) + } + if mediaUpdated { + mediaNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), photoReference: .webPage(webPage: webpageReference, media: image))) + + if let smallest = smallestImageRepresentation(image.representations) { + updatedFetchSignal = fetchedMediaResource( + mediaBox: context.account.postbox.mediaBox, + userLocation: .peer(message.id.peerId), + userContentType: .image, + reference: .media(media: .webPage(webPage: webpageReference, media: image), resource: smallest.resource), + statsCategory: .image + ) + |> map { _ -> Void in + return Void() + } + |> `catch` { _ -> Signal in + return .complete() + } + } + } + mediaWebpageIconTintColor = .white + mediaWebpageHasImageThumbnail = true + } else { + let mediaAccentColor = incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.secondaryTextColor + if mediaUpdated || themeUpdated { + let backgroundColor = mediaAccentColor.withAlphaComponent(0.1) + mediaNode.setSignal(.single({ arguments in + let size = arguments.imageSize + let context = DrawingContext(size: size)! + context.withFlippedContext { context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(backgroundColor.cgColor) + context.addPath(CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil)) + context.fillPath() + } + return context + })) + } + mediaWebpageIconTintColor = mediaAccentColor + } } else if let map = media as? TelegramMediaMap { - if previousMedia?.isEqual(to: media) != true { + if mediaUpdated { let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(ChatMessagePollOptionNode.mediaSize.width), height: Int32(ChatMessagePollOptionNode.mediaSize.height)) mediaNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource)) } @@ -1226,6 +1342,18 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { )) apply() + if let mediaWebpageIconTintColor { + if mediaWebpageHasImageThumbnail { + node.updateMediaWebpageOverlayNode(frame: mediaFrame) + } else { + node.removeMediaWebpageOverlayNode() + } + node.updateMediaWebpageIconNode(frame: mediaFrame, tintColor: mediaWebpageIconTintColor) + } else { + node.removeMediaWebpageOverlayNode() + node.removeMediaWebpageIconNode() + } + if isVideo { let mediaVideoIconNode: ASImageNode if let current = node.mediaVideoIconNode { @@ -1247,6 +1375,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } else if let mediaNode = node.mediaNode { mediaNode.removeFromSupernode() node.mediaNode = nil + node.removeMediaWebpageOverlayNode() + node.removeMediaWebpageIconNode() if let mediaVideoIconNode = node.mediaVideoIconNode { mediaVideoIconNode.removeFromSupernode() node.mediaVideoIconNode = nil @@ -1254,6 +1384,11 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { } else if let mediaVideoIconNode = node.mediaVideoIconNode { mediaVideoIconNode.removeFromSupernode() node.mediaVideoIconNode = nil + node.removeMediaWebpageOverlayNode() + node.removeMediaWebpageIconNode() + } else { + node.removeMediaWebpageOverlayNode() + node.removeMediaWebpageIconNode() } node.setMediaHidden(node.mediaHidden) @@ -1386,7 +1521,10 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { private var animationLayer: InlineStickerItemLayer? private var statusNode: RadialStatusNode? private var videoIconView: UIImageView? + private var webpageOverlayView: UIView? + private var webpageIconView: UIImageView? private var appliedMedia: AnyMediaReference? + private var appliedWebpagePlaceholderColor: UIColor? private var currentFont: UIFont? private var currentTextColor: UIColor? @@ -1654,6 +1792,61 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { @objc private func attachButtonPressed() { self.attachPressed?() } + + private func updateWebpageIconView(frame: CGRect, tintColor: UIColor) { + let webpageIconView: UIImageView + if let current = self.webpageIconView { + webpageIconView = current + } else { + let current = UIImageView(image: UIImage(bundleImageName: "Chat/Context Menu/Link")?.withRenderingMode(.alwaysTemplate)) + current.isUserInteractionEnabled = false + self.view.addSubview(current) + self.webpageIconView = current + webpageIconView = current + } + + webpageIconView.tintColor = tintColor + let iconSize = webpageIconView.image?.size ?? CGSize(width: 30.0, height: 30.0) + webpageIconView.frame = CGRect(origin: CGPoint(x: frame.midX - iconSize.width * 0.5, y: frame.midY - iconSize.height * 0.5), size: iconSize) + webpageIconView.isHidden = false + self.view.bringSubviewToFront(webpageIconView) + } + + private func removeWebpageIconView() { + if let webpageIconView = self.webpageIconView { + self.webpageIconView = nil + webpageIconView.removeFromSuperview() + } + } + + private func updateWebpageOverlayView(frame: CGRect) { + let webpageOverlayView: UIView + if let current = self.webpageOverlayView { + webpageOverlayView = current + } else { + let current = UIView() + current.isUserInteractionEnabled = false + current.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) + current.layer.cornerRadius = 10.0 + current.clipsToBounds = true + self.view.addSubview(current) + self.webpageOverlayView = current + webpageOverlayView = current + } + + webpageOverlayView.frame = frame + webpageOverlayView.isHidden = false + if let webpageIconView = self.webpageIconView { + self.view.insertSubview(webpageOverlayView, belowSubview: webpageIconView) + } + } + + private func removeWebpageOverlayView() { + if let webpageOverlayView = self.webpageOverlayView { + self.webpageOverlayView = nil + webpageOverlayView.removeFromSuperview() + } + } private func updateModeSelectorLayout(size: CGSize, theme: PresentationTheme?, animated: Bool) { guard !size.width.isZero, !size.height.isZero, let theme = self.currentTheme else { @@ -1791,6 +1984,9 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { } self.imageButton.frame = imageNodeFrame self.imageButton.isHidden = false + self.removeWebpageOverlayView() + self.removeWebpageIconView() + self.appliedWebpagePlaceholderColor = nil } else if let animationLayer = self.animationLayer { self.animationLayer = nil animationLayer.removeFromSuperlayer() @@ -1817,11 +2013,14 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { } var isVideo = false + var webpageIconTintColor: UIColor? + var webpageHasImageThumbnail = false if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations), let photoReference = media.concrete(TelegramMediaImage.self) { imageSize = largest.dimensions.cgSize.aspectFilled(imageNodeSize) if updateMedia { imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: .other, photoReference: photoReference)) } + self.appliedWebpagePlaceholderColor = nil } else if let file = media.media as? TelegramMediaFile, let fileReference = media.concrete(TelegramMediaFile.self) { if let dimensions = file.dimensions { imageSize = dimensions.cgSize.aspectFilled(imageNodeSize) @@ -1836,12 +2035,50 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { } isVideo = true } + self.appliedWebpagePlaceholderColor = nil + } else if let webpage = media.media as? TelegramMediaWebpage { + let webpageReference = WebpageReference(webpage) + if case let .Loaded(content) = webpage.content, let image = content.image { + if let largest = largestImageRepresentation(image.representations) { + imageSize = largest.dimensions.cgSize.aspectFilled(imageNodeSize) + } + if updateMedia { + imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: .other, photoReference: .webPage(webPage: webpageReference, media: image))) + if let representation = smallestImageRepresentation(image.representations) { + let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: .media(media: .webPage(webPage: webpageReference, media: image), resource: representation.resource)).startStandalone() + } + } + self.appliedWebpagePlaceholderColor = nil + webpageIconTintColor = .white + webpageHasImageThumbnail = true + } else { + let mediaAccentColor = self.currentIncoming ? theme.chat.message.incoming.accentTextColor : theme.chat.message.outgoing.secondaryTextColor + let backgroundColor = mediaAccentColor.withAlphaComponent(0.1) + if updateMedia || self.appliedWebpagePlaceholderColor?.isEqual(backgroundColor) != true { + self.appliedWebpagePlaceholderColor = backgroundColor + imageNode.setSignal(.single({ arguments in + let size = arguments.imageSize + let context = DrawingContext(size: size)! + context.withFlippedContext { context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(backgroundColor.cgColor) + context.addPath(CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil)) + context.fillPath() + } + return context + })) + } + webpageIconTintColor = mediaAccentColor + } } else if let map = media.media as? TelegramMediaMap { imageSize = imageNodeSize if updateMedia { let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(imageSize.width), height: Int32(imageSize.height)) imageNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource)) } + self.appliedWebpagePlaceholderColor = nil + } else { + self.appliedWebpagePlaceholderColor = nil } let apply = imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 10.0), imageSize: imageSize, boundingSize: imageNodeSize, intrinsicInsets: UIEdgeInsets(), emptyColor: theme.list.mediaPlaceholderColor)) @@ -1866,10 +2103,23 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { statusNode.frame = imageNodeFrame.insetBy(dx: 4.0, dy: 4.0) statusNode.transitionToState(.progress(color: .white, lineWidth: 2.0, value: max(0.027, min(1.0, progress)), cancelEnabled: true, animateRotation: false)) isVideo = false + self.removeWebpageOverlayView() } else if let statusNode = self.statusNode { self.statusNode = nil statusNode.removeFromSupernode() } + + if attachment.progress == nil, let webpageIconTintColor { + if webpageHasImageThumbnail { + self.updateWebpageOverlayView(frame: imageNodeFrame) + } else { + self.removeWebpageOverlayView() + } + self.updateWebpageIconView(frame: imageNodeFrame, tintColor: webpageIconTintColor) + } else { + self.removeWebpageOverlayView() + self.removeWebpageIconView() + } if isVideo { let videoIconView: UIImageView @@ -1890,6 +2140,7 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { } } else { self.appliedMedia = nil + self.appliedWebpagePlaceholderColor = nil if let imageNode = self.imageNode { self.imageNode = nil imageNode.removeFromSupernode() @@ -1902,6 +2153,8 @@ private final class ChatMessagePollAddOptionNode: ASDisplayNode { self.videoIconView = nil videoIconView.removeFromSuperview() } + self.removeWebpageOverlayView() + self.removeWebpageIconView() self.imageButton.removeFromSupernode() } } @@ -2241,7 +2494,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { context: item.context, updatedPresentationData: item.controllerInteraction.updatedPresentationData, subject: .option, - availableButtons: [.gallery, .sticker, .location], + availableButtons: [.gallery, .sticker, .location, .link], present: { [weak item] controller, _ in item?.controllerInteraction.navigationController()?.pushViewController(controller) }, @@ -2687,17 +2940,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { var isRestricted = false if let poll = poll { - if !poll.countries.isEmpty, let accountCountry = item.associatedData.accountCountry, !poll.countries.contains(accountCountry) { - isRestricted = true - } - if poll.restrictToSubscribers { - let period: Int32 = item.context.account.testingEnvironment ? 5 * 60 : 24 * 60 * 60 - if !item.associatedData.isParticipant { - isRestricted = true - } else if let invitedOn = item.associatedData.invitedOn, invitedOn + period > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { - isRestricted = true - } - } + isRestricted = item.associatedData.isPollVotingRestricted( + poll: poll, + accountTestingEnvironment: item.context.account.testingEnvironment, + currentTimestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + ) orderedPollOptions = resolvedOptionOrder(for: item) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift index b76dd20221..adb73ad6fd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift @@ -135,21 +135,6 @@ private final class GlassButtonView: UIView { super.init(frame: frame) self.addSubview(self.backgroundView) - - if #available(iOS 26.0, *) { - } else { - self.button.highligthedChanged = { [weak self] highlighted in - guard let self else { - return - } - if highlighted && self.isEnabled && !self.isImplicitlyDisabled { - self.backgroundView.contentView.alpha = 0.6 - } else { - self.backgroundView.contentView.alpha = 1.0 - self.backgroundView.contentView.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - } - } - } } required init?(coder: NSCoder) { @@ -176,7 +161,8 @@ private final class GlassButtonView: UIView { transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: params.size)) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) - self.backgroundView.update(size: params.size, cornerRadius: min(params.size.width, params.size.height) * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: params.preferClearGlass ? .clear : .panel), isInteractive: isEnabled, transition: transition) + self.backgroundView.update(size: params.size, cornerRadius: min(params.size.width, params.size.height) * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: params.preferClearGlass ? .clear : .panel), isInteractive: true, transition: transition) + self.backgroundView.isUserInteractionEnabled = isEnabled self.iconView.alpha = isEnabled ? 1.0 : 0.5 self.iconView.tintMask.alpha = self.iconView.alpha diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD index 393edc5f09..a322282d44 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD @@ -16,11 +16,11 @@ swift_library( "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", - "//submodules/SolidRoundedButtonNode", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", "//submodules/TelegramPresentationData", "//submodules/TelegramUIPreferences", "//submodules/TelegramNotices", - "//submodules/PresentationDataUtils", "//submodules/AnimationUI", "//submodules/MergeLists", "//submodules/MediaResources", @@ -45,6 +45,7 @@ swift_library( "//submodules/AnimatedCountLabelNode", "//submodules/HexColor", "//submodules/QrCodeUI", + "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/Components/BundleIconComponent", "//submodules/Components/SheetComponent", diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index b9efa55c34..b32cf1027d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift @@ -7,11 +7,9 @@ import Postbox import TelegramCore import SwiftSignalKit import AccountContext -import SolidRoundedButtonNode import TelegramPresentationData import TelegramUIPreferences import TelegramNotices -import PresentationDataUtils import AnimationUI import MergeLists import MediaResources @@ -36,6 +34,8 @@ import AnimatedCountLabelNode import HexColor import QrCodeUI import ComponentFlow +import ButtonComponent +import ComponentDisplayAdapters import GlassBarButtonComponent import SheetComponent import BundleIconComponent @@ -245,7 +245,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec if let image = cachedBorderImages[key] { return image } else { - let image = generateImage(CGSize(width: 18.0, height: 18.0), rotatedContext: { size, context in + let image = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -271,7 +271,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec context.setLineWidth(lineWidth) context.strokeEllipse(in: bounds.insetBy(dx: 1.0 + lineWidth / 2.0, dy: 1.0 + lineWidth / 2.0)) } - })?.stretchableImage(withLeftCapWidth: 9, topCapHeight: 9) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 16) cachedBorderImages[key] = image return image } @@ -314,7 +314,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { self.imageNode = TransformImageNode() self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 82.0, height: 108.0)) self.imageNode.isLayerBacked = true - self.imageNode.cornerRadius = 8.0 + //self.imageNode.cornerRadius = 16.0 self.imageNode.clipsToBounds = true self.overlayNode = ASImageNode() @@ -488,7 +488,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { let imageSize = CGSize(width: 82.0, height: 108.0) strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 4.0, y: 6.0), size: imageSize) - let applyLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) + let applyLayout = makeImageLayout(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: .clear)) applyLayout() strongSelf.overlayNode.frame = strongSelf.imageNode.frame.insetBy(dx: -1.0, dy: -1.0) @@ -770,8 +770,11 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg private let switchThemeButtonNode: HighlightTrackingButtonNode private let animationContainerNode: ASDisplayNode private var animationNode: AnimationNode - private let doneButton: SolidRoundedButtonNode - private let scanButton: SolidRoundedButtonNode + private let doneButton = ComponentView() + private let scanButton = ComponentView() + private let doneButtonTitle: String + private let scanButtonTitle: String + private let fileName: String private let listNode: ListView private var entries: [ThemeSettingsThemeEntry]? @@ -811,11 +814,20 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.context = context self.controller = controller self.presentationData = presentationData + self.fileName = controller.subject.fileName + self.scanButtonTitle = presentationData.strings.PeerInfo_QRCode_Scan + switch controller.subject { + case .peer: + self.doneButtonTitle = presentationData.strings.InviteLink_QRCode_Share + case .messages: + self.doneButtonTitle = presentationData.strings.Share_ShareMessage + } self.wrappingScrollNode = ASScrollNode() self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.scrollNodeContentNode = ASDisplayNode() self.scrollNodeContentNode.clipsToBounds = true @@ -870,19 +882,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) self.animationNode.isUserInteractionEnabled = false - - self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), glass: true, height: 52.0, cornerRadius: 26.0) - switch controller.subject { - case .peer: - self.doneButton.title = self.presentationData.strings.InviteLink_QRCode_Share - case .messages: - self.doneButton.title = self.presentationData.strings.Share_ShareMessage - } - - self.scanButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor), font: .regular, height: 42.0, cornerRadius: 0.0) - self.scanButton.title = presentationData.strings.PeerInfo_QRCode_Scan - self.scanButton.icon = UIImage(bundleImageName: "Settings/ScanQr") - + self.listNode = ListViewImpl() self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) @@ -903,8 +903,6 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.contentContainerNode.addSubnode(self.titleNode) self.contentContainerNode.addSubnode(self.segmentedNode) - self.contentContainerNode.addSubnode(self.doneButton) - self.contentContainerNode.addSubnode(self.scanButton) self.topContentContainerNode.addSubnode(self.animationContainerNode) self.animationContainerNode.addSubnode(self.animationNode) @@ -927,74 +925,6 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg } } - let fileName = controller.subject.fileName - self.doneButton.pressed = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.doneButton.isUserInteractionEnabled = false - - if strongSelf.segmentedNode.selectedIndex == 0 { - strongSelf.contentNode.generateVideo { [weak self] url in - if let strongSelf = self { - let tempFilePath = NSTemporaryDirectory() + "\(fileName).mp4" - try? FileManager.default.removeItem(atPath: tempFilePath) - let tempFileUrl = URL(fileURLWithPath: tempFilePath) - try? FileManager.default.moveItem(at: url, to: tempFileUrl) - - let activityController = UIActivityViewController(activityItems: [tempFileUrl], applicationActivities: [ShareToInstagramActivity(context: strongSelf.context)]) - activityController.completionWithItemsHandler = { [weak self] _, finished, _, _ in - if let strongSelf = self { - if finished { - strongSelf.completion?(strongSelf.selectedEmoticon) - } else { - strongSelf.doneButton.isUserInteractionEnabled = true - } - } - } - if let window = strongSelf.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) - } - } - } else { - strongSelf.contentNode.generateImage { [weak self] image in - if let strongSelf = self, let image = image, let jpgData = image.jpegData(compressionQuality: 0.9) { - let tempFilePath = NSTemporaryDirectory() + "\(fileName).jpg" - try? FileManager.default.removeItem(atPath: tempFilePath) - let tempFileUrl = URL(fileURLWithPath: tempFilePath) - try? jpgData.write(to: tempFileUrl) - - let activityController = UIActivityViewController(activityItems: [tempFileUrl], applicationActivities: [ShareToInstagramActivity(context: strongSelf.context)]) - activityController.completionWithItemsHandler = { [weak self] _, finished, _, _ in - if let strongSelf = self { - if finished { - strongSelf.completion?(strongSelf.selectedEmoticon) - } else { - strongSelf.doneButton.isUserInteractionEnabled = true - } - } - } - if let window = strongSelf.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) - } - } - } - } - - self.scanButton.pressed = { [weak self] in - guard let self else { - return - } - let controller = QrCodeScanScreen(context: self.context, subject: .peer) - self.controller?.push(controller) - } - let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) |> map { animatedEmoji -> [String: [StickerPackItem]] in var animatedEmojiStickers: [String: [StickerPackItem]] = [:] @@ -1259,8 +1189,6 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg } self.cancelButtonNode.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) - self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) - self.scanButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor)) let previousIconColors = iconColors(theme: previousTheme) let newIconColors = iconColors(theme: self.presentationData.theme) @@ -1299,6 +1227,67 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.cancel?() } + private func doneButtonPressed() { + self.doneButton.view?.isUserInteractionEnabled = false + + if self.segmentedNode.selectedIndex == 0 { + self.contentNode.generateVideo { [weak self] url in + if let strongSelf = self { + let tempFilePath = NSTemporaryDirectory() + "\(strongSelf.fileName).mp4" + try? FileManager.default.removeItem(atPath: tempFilePath) + let tempFileUrl = URL(fileURLWithPath: tempFilePath) + try? FileManager.default.moveItem(at: url, to: tempFileUrl) + + let activityController = UIActivityViewController(activityItems: [tempFileUrl], applicationActivities: [ShareToInstagramActivity(context: strongSelf.context)]) + activityController.completionWithItemsHandler = { [weak self] _, finished, _, _ in + if let strongSelf = self { + if finished { + strongSelf.completion?(strongSelf.selectedEmoticon) + } else { + strongSelf.doneButton.view?.isUserInteractionEnabled = true + } + } + } + if let window = strongSelf.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)) + } + strongSelf.context.sharedContext.applicationBindings.presentNativeController(activityController) + } + } + } else { + self.contentNode.generateImage { [weak self] image in + if let strongSelf = self, let image = image, let jpgData = image.jpegData(compressionQuality: 0.9) { + let tempFilePath = NSTemporaryDirectory() + "\(strongSelf.fileName).jpg" + try? FileManager.default.removeItem(atPath: tempFilePath) + let tempFileUrl = URL(fileURLWithPath: tempFilePath) + try? jpgData.write(to: tempFileUrl) + + let activityController = UIActivityViewController(activityItems: [tempFileUrl], applicationActivities: [ShareToInstagramActivity(context: strongSelf.context)]) + activityController.completionWithItemsHandler = { [weak self] _, finished, _, _ in + if let strongSelf = self { + if finished { + strongSelf.completion?(strongSelf.selectedEmoticon) + } else { + strongSelf.doneButton.view?.isUserInteractionEnabled = true + } + } + } + if let window = strongSelf.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)) + } + strongSelf.context.sharedContext.applicationBindings.presentNativeController(activityController) + } + } + } + } + + private func scanButtonPressed() { + let controller = QrCodeScanScreen(context: self.context, subject: .peer) + self.controller?.push(controller) + } + @objc private func switchThemePressed() { self.switchThemeButtonNode.isUserInteractionEnabled = false Queue.mainQueue().after(0.5) { @@ -1352,11 +1341,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg }) } -// Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) { -// let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear -// self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor -// self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration) -// } + if let snapshotView = self.contentBackgroundView.snapshotView(afterScreenUpdates: false) { + self.contentBackgroundView.superview?.insertSubview(snapshotView, aboveSubview: self.contentBackgroundView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: ChatQrCodeScreenImpl.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.contentContainerNode.frame @@ -1435,13 +1425,13 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) - var insets = layout.insets(options: [.statusBar, .input]) let cleanInsets = layout.insets(options: [.statusBar]) - insets.top = max(10.0, insets.top) - let bottomInset: CGFloat = 10.0 + cleanInsets.bottom let titleHeight: CGFloat = 54.0 - let contentHeight = titleHeight + bottomInset + 188.0 + 52.0 + let buttonHeight: CGFloat = 52.0 + let buttonSpacing: CGFloat = 8.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: cleanInsets.bottom, innerDiameter: buttonHeight, sideInset: 30.0) + let contentHeight = titleHeight + 10.0 + 188.0 + buttonHeight + buttonSpacing + buttonInsets.bottom let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) @@ -1459,14 +1449,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.contentBackgroundView.update(size: contentFrame.size, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, topCornerRadius: 38.0, bottomCornerRadius: layout.deviceMetrics.screenCornerRadius - 2.0, transition: .immediate) transition.updateFrame(view: self.contentBackgroundView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - let barButtonSize = CGSize(width: 40.0, height: 40.0) + let barButtonSize = CGSize(width: 44.0, height: 44.0) let cancelButtonSize = self.cancelButton.update( transition: .immediate, component: AnyComponent(GlassBarButtonComponent( size: barButtonSize, - backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: self.presentationData.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -1493,9 +1483,9 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg transition: .immediate, component: AnyComponent(GlassBarButtonComponent( size: barButtonSize, - backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: self.presentationData.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "switchTheme", component: AnyComponent( Rectangle(color: .clear) )), @@ -1517,7 +1507,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg } transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.scrollNodeContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height + 2000.0))) + transition.updateFrame(node: self.scrollNodeContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height))) let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight)) let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 36.0 - titleSize.height / 2.0), size: titleSize) @@ -1536,12 +1526,83 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame) - let buttonInset: CGFloat = 30.0 - let scanButtonHeight = self.scanButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom, width: contentFrame.width, height: scanButtonHeight)) + let buttonTransition = ComponentTransition(transition) + let buttonWidth = contentFrame.width - buttonInsets.left - buttonInsets.right + let primaryBackgroundColor = self.presentationData.theme.list.itemCheckColors.fillColor + let primaryForegroundColor = self.presentationData.theme.list.itemCheckColors.foregroundColor + let doneButtonSize = self.doneButton.update( + transition: buttonTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: primaryBackgroundColor, + foreground: primaryForegroundColor, + pressedColor: primaryBackgroundColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: self.doneButtonTitle, font: Font.semibold(17.0), color: primaryForegroundColor) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + self?.doneButtonPressed() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: buttonHeight) + ) + + let secondaryForegroundColor = self.presentationData.theme.list.itemCheckColors.fillColor + let scanButtonContents: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + BundleIconComponent(name: "Settings/ScanQr", tintColor: secondaryForegroundColor) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + Text(text: self.scanButtonTitle, font: Font.semibold(17.0), color: secondaryForegroundColor) + )) + ] + let scanButtonSize = self.scanButton.update( + transition: buttonTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: secondaryForegroundColor.withMultipliedAlpha(0.1), + foreground: secondaryForegroundColor, + pressedColor: secondaryForegroundColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + HStack(scanButtonContents, spacing: 6.0) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + self?.scanButtonPressed() + } + )), + environment: {}, + containerSize: CGSize(width: buttonWidth, height: buttonHeight) + ) - let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom, width: contentFrame.width, height: doneButtonHeight)) + let scanButtonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight - scanButtonSize.height - buttonInsets.bottom), size: scanButtonSize) + let doneButtonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: scanButtonFrame.minY - buttonSpacing - doneButtonSize.height), size: doneButtonSize) + if let doneButtonView = self.doneButton.view { + if doneButtonView.superview == nil { + self.contentContainerNode.view.addSubview(doneButtonView) + } + doneButtonView.isAccessibilityElement = true + doneButtonView.accessibilityLabel = self.doneButtonTitle + doneButtonView.accessibilityTraits = [.button] + transition.updateFrame(view: doneButtonView, frame: doneButtonFrame) + } + if let scanButtonView = self.scanButton.view { + if scanButtonView.superview == nil { + self.contentContainerNode.view.addSubview(scanButtonView) + } + scanButtonView.isAccessibilityElement = true + scanButtonView.accessibilityLabel = self.scanButtonTitle + scanButtonView.accessibilityTraits = [.button] + transition.updateFrame(view: scanButtonView, frame: scanButtonFrame) + } transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index b15afc9f91..6d6472272f 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/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 82934145c6..bdf4739e9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -313,7 +313,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if let context = self?.context, let navigationController = self?.getNavigationController() { let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .always, animated: true).startStandalone() } - }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false + }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, editGif: { _, _ in }, requestMessageActionCallback: { [weak self] message, _, _, _, _ in guard let self else { @@ -327,7 +327,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url in self?.openUrl(url.url, progress: url.progress) }, openExternalInstantPage: { _ in - }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in + }, shareCurrentLocation: { _ in }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType) { navigationController.pushViewController(controller) @@ -611,7 +611,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift new file mode 100644 index 0000000000..c85f7a8a68 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift @@ -0,0 +1,503 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import ItemListPeerItem + +private final class ChatRecentActionsFilterControllerArguments { + let context: AccountContext + + let toggleAllActions: (Bool) -> Void + let toggleAction: ([AdminLogEventsFlags]) -> Void + let toggleAllAdmins: (Bool) -> Void + let toggleAdmin: (PeerId) -> Void + + init(context: AccountContext, toggleAllActions: @escaping (Bool) -> Void, toggleAction: @escaping ([AdminLogEventsFlags]) -> Void, toggleAllAdmins: @escaping (Bool) -> Void, toggleAdmin: @escaping (PeerId) -> Void) { + self.context = context + self.toggleAllActions = toggleAllActions + self.toggleAction = toggleAction + self.toggleAllAdmins = toggleAllAdmins + self.toggleAdmin = toggleAdmin + } +} + +private enum ChatRecentActionsFilterSection: Int32 { + case actions + case admins +} + +private enum ChatRecentActionsFilterEntryStableId: Hashable { + case index(Int32) + case peer(PeerId) +} + +private enum ChatRecentActionsFilterEntry: ItemListNodeEntry { + case actionsTitle(PresentationTheme, String) + case allActions(PresentationTheme, String, Bool) + case actionItem(PresentationTheme, Int32, [AdminLogEventsFlags], String, Bool) + + case adminsTitle(PresentationTheme, String) + case allAdmins(PresentationTheme, String, Bool) + case adminPeerItem(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Int32, RenderedChannelParticipant, Bool, Bool) + + var section: ItemListSectionId { + switch self { + case .actionsTitle, .allActions, .actionItem: + return ChatRecentActionsFilterSection.actions.rawValue + case .adminsTitle, .allAdmins, .adminPeerItem: + return ChatRecentActionsFilterSection.admins.rawValue + } + } + + var stableId: ChatRecentActionsFilterEntryStableId { + switch self { + case .actionsTitle: + return .index(0) + case .allActions: + return .index(1) + case let .actionItem(_, index, _, _, _): + return .index(100 + index) + case .adminsTitle: + return .index(200) + case .allAdmins: + return .index(201) + case let .adminPeerItem(_, _, _, _, _, participant, _, _): + return .peer(participant.peer.id) + } + } + + static func ==(lhs: ChatRecentActionsFilterEntry, rhs: ChatRecentActionsFilterEntry) -> Bool { + switch lhs { + case let .actionsTitle(lhsTheme, lhsText): + if case let .actionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .allActions(lhsTheme, lhsText, lhsValue): + if case let .allActions(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .actionItem(lhsTheme, lhsIndex, lhsFlags, lhsText, lhsValue): + if case let .actionItem(rhsTheme, rhsIndex, rhsFlags, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsFlags == rhsFlags, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .adminsTitle(lhsTheme, lhsText): + if case let .adminsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .allAdmins(lhsTheme, lhsText, lhsValue): + if case let .allAdmins(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .adminPeerItem(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameDisplayOrder, lhsIndex, lhsParticipant, lhsIsAntiSpam, lhsChecked): + if case let .adminPeerItem(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameDisplayOrder, rhsIndex, rhsParticipant, rhsIsAntiSpam, rhsChecked) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsDateTimeFormat != rhsDateTimeFormat { + return false + } + if lhsNameDisplayOrder != rhsNameDisplayOrder { + return false + } + if lhsIndex != rhsIndex { + return false + } + if lhsParticipant != rhsParticipant { + return false + } + if lhsIsAntiSpam != rhsIsAntiSpam { + return false + } + if lhsChecked != rhsChecked { + return false + } + return true + } else { + return false + } + } + } + + static func <(lhs: ChatRecentActionsFilterEntry, rhs: ChatRecentActionsFilterEntry) -> Bool { + switch lhs { + case .actionsTitle: + return true + case .allActions: + switch rhs { + case .actionsTitle: + return false + default: + return true + } + case let .actionItem(_, lhsIndex, _, _, _): + switch rhs { + case .actionsTitle, .allActions: + return false + case let .actionItem(_, rhsIndex, _, _, _): + return lhsIndex < rhsIndex + default: + return true + } + case .adminsTitle: + switch rhs { + case .adminPeerItem, .allAdmins: + return true + default: + return false + } + case .allAdmins: + switch rhs { + case .adminPeerItem: + return true + default: + return false + } + case let .adminPeerItem(_, _, _, _, lhsIndex, _, _, _): + switch rhs { + case let .adminPeerItem(_, _, _, _, rhsIndex, _, _, _): + return lhsIndex < rhsIndex + default: + return false + } + } + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! ChatRecentActionsFilterControllerArguments + switch self { + case let .actionsTitle(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .allActions(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleAllActions(value) + }) + case let .actionItem(_, _, events, text, value): + return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .right, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.toggleAction(events) + }) + case let .adminsTitle(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .allAdmins(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleAllAdmins(value) + }) + case let .adminPeerItem(_, strings, dateTimeFormat, nameDisplayOrder, _, participant, isAntiSpam, checked): + var peerText: String = "" + if isAntiSpam { + peerText = strings.Group_Management_AntiSpamMagic + } else { + switch participant.participant { + case .creator: + peerText = strings.Channel_Management_LabelOwner.lowercased() + case .member: + peerText = strings.ChatAdmins_AdminLabel.lowercased() + } + } + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: .text(peerText, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, selectable: true, sectionId: self.section, action: { + arguments.toggleAdmin(participant.peer.id) + }, setPeerIdWithRevealedOptions: { _, _ in + }, removePeer: { _ in }) + } + } +} + +private struct ChatRecentActionsFilterControllerState: Equatable { + let events: AdminLogEventsFlags + let adminPeerIds: [PeerId]? + + init(events: AdminLogEventsFlags, adminPeerIds: [PeerId]?) { + self.events = events + self.adminPeerIds = adminPeerIds + } + + static func ==(lhs: ChatRecentActionsFilterControllerState, rhs: ChatRecentActionsFilterControllerState) -> Bool { + if lhs.events != rhs.events { + return false + } + if let lhsAdminPeerIds = lhs.adminPeerIds, let rhsAdminPeerIds = rhs.adminPeerIds { + if lhsAdminPeerIds != rhsAdminPeerIds { + return false + } + } else if (lhs.adminPeerIds != nil) != (rhs.adminPeerIds != nil) { + return false + } + + return true + } + + func withUpdatedEvents(_ events: AdminLogEventsFlags) -> ChatRecentActionsFilterControllerState { + return ChatRecentActionsFilterControllerState(events: events, adminPeerIds: self.adminPeerIds) + } + + func withUpdatedAdminPeerIds(_ adminPeerIds: [PeerId]?) -> ChatRecentActionsFilterControllerState { + return ChatRecentActionsFilterControllerState(events: self.events, adminPeerIds: adminPeerIds) + } +} + +private func channelRecentActionsFilterControllerEntries(presentationData: PresentationData, accountPeerId: PeerId, peer: Peer, antiSpamBotId: PeerId?, state: ChatRecentActionsFilterControllerState, participants: [RenderedChannelParticipant]?) -> [ChatRecentActionsFilterEntry] { + var isGroup = true + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + isGroup = false + } + + var entries: [ChatRecentActionsFilterEntry] = [] + + let order: [([AdminLogEventsFlags], String)] + if isGroup { + order = [ + ([.ban, .unban, .kick, .unkick], presentationData.strings.Channel_AdminLogFilter_EventsRestrictions), + ([.promote, .demote], presentationData.strings.Channel_AdminLogFilter_EventsAdmins), + ([.invite, .join], presentationData.strings.Channel_AdminLogFilter_EventsNewMembers), + ([.info, .settings], isGroup ? presentationData.strings.Channel_AdminLogFilter_EventsInfo : presentationData.strings.Channel_AdminLogFilter_ChannelEventsInfo), + ([.invites], presentationData.strings.Channel_AdminLogFilter_EventsInviteLinks), + ([.deleteMessages], presentationData.strings.Channel_AdminLogFilter_EventsDeletedMessages), + ([.editMessages], presentationData.strings.Channel_AdminLogFilter_EventsEditedMessages), + ([.pinnedMessages], presentationData.strings.Channel_AdminLogFilter_EventsPinned), + ([.leave], presentationData.strings.Channel_AdminLogFilter_EventsLeaving), + ([.calls], presentationData.strings.Channel_AdminLogFilter_EventsCalls) + ] + } else { + order = [ + ([.promote, .demote], presentationData.strings.Channel_AdminLogFilter_EventsAdmins), + ([.invite, .join], presentationData.strings.Channel_AdminLogFilter_EventsNewMembers), + ([.info, .settings], isGroup ? presentationData.strings.Channel_AdminLogFilter_EventsInfo : presentationData.strings.Channel_AdminLogFilter_ChannelEventsInfo), + ([.invites], presentationData.strings.Channel_AdminLogFilter_EventsInviteLinks), + ([.deleteMessages], presentationData.strings.Channel_AdminLogFilter_EventsDeletedMessages), + ([.editMessages], presentationData.strings.Channel_AdminLogFilter_EventsEditedMessages), + ([.pinnedMessages], presentationData.strings.Channel_AdminLogFilter_EventsPinned), + ([.leave], presentationData.strings.Channel_AdminLogFilter_EventsLeaving), + ([.calls], presentationData.strings.Channel_AdminLogFilter_EventsLiveStreams) + ] + } + + var allTypesSelected = true + outer: for (events, _) in order { + for event in events { + if !state.events.contains(event) { + allTypesSelected = false + break outer + } + } + } + + entries.append(.actionsTitle(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_EventsTitle)) + entries.append(.allActions(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_EventsAll, allTypesSelected)) + + var index: Int32 = 0 + for (events, text) in order { + var eventsSelected = true + inner: for event in events { + if !state.events.contains(event) { + eventsSelected = false + break inner + } + } + entries.append(.actionItem(presentationData.theme, index, events, text, eventsSelected)) + index += 1 + } + + if let participants = participants { + var allAdminsSelected = true + if let adminPeerIds = state.adminPeerIds { + for participant in participants { + if !adminPeerIds.contains(participant.peer.id) { + allAdminsSelected = false + break + } + } + } else { + allAdminsSelected = true + } + + entries.append(.adminsTitle(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_AdminsTitle)) + entries.append(.allAdmins(presentationData.theme, presentationData.strings.Channel_AdminLogFilter_AdminsAll, allAdminsSelected)) + + var index: Int32 = 0 + for participant in participants { + var adminSelected = true + if let adminPeerIds = state.adminPeerIds { + if !adminPeerIds.contains(participant.peer.id) { + adminSelected = false + } + } else { + adminSelected = true + } + entries.append(.adminPeerItem(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, index, participant, participant.peer.id == antiSpamBotId, adminSelected)) + index += 1 + } + } + + return entries +} + +public func channelRecentActionsFilterController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: Peer, events: AdminLogEventsFlags, adminPeerIds: [PeerId]?, apply: @escaping (_ events: AdminLogEventsFlags, _ adminPeerIds: [PeerId]?) -> Void) -> ViewController { + let statePromise = ValuePromise(ChatRecentActionsFilterControllerState(events: events, adminPeerIds: adminPeerIds), ignoreRepeated: true) + let stateValue = Atomic(value: ChatRecentActionsFilterControllerState(events: events, adminPeerIds: adminPeerIds)) + let updateState: ((ChatRecentActionsFilterControllerState) -> ChatRecentActionsFilterControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var dismissImpl: (() -> Void)? + + let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil) + + let actionsDisposable = DisposableSet() + + let arguments = ChatRecentActionsFilterControllerArguments(context: context, toggleAllActions: { value in + updateState { current in + if value { + return current.withUpdatedEvents(.all) + } else { + return current.withUpdatedEvents([]) + } + } + }, toggleAction: { events in + if let first = events.first { + updateState { current in + var updatedEvents = current.events + if updatedEvents.contains(first) { + for event in events { + updatedEvents.remove(event) + } + } else { + for event in events { + updatedEvents.insert(event) + } + } + return current.withUpdatedEvents(updatedEvents) + } + } + }, toggleAllAdmins: { value in + let _ = (adminsPromise.get() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { admins in + if let _ = admins { + updateState { current in + if value { + return current.withUpdatedAdminPeerIds(nil) + } else { + return current.withUpdatedAdminPeerIds([]) + } + } + } + }) + }, toggleAdmin: { adminId in + let _ = (adminsPromise.get() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { admins in + if let admins = admins { + updateState { current in + if let adminPeerIds = current.adminPeerIds, let index = adminPeerIds.firstIndex(of: adminId) { + var updatedAdminPeerIds = adminPeerIds + updatedAdminPeerIds.remove(at: index) + return current.withUpdatedAdminPeerIds(updatedAdminPeerIds) + } else { + var updatedAdminPeerIds = current.adminPeerIds ?? admins.map { $0.peer.id } + if updatedAdminPeerIds.contains(adminId), let index = updatedAdminPeerIds.firstIndex(of: adminId) { + updatedAdminPeerIds.remove(at: index) + } else { + updatedAdminPeerIds.append(adminId) + } + return current.withUpdatedAdminPeerIds(updatedAdminPeerIds) + } + } + } + }) + }) + + adminsPromise.set(.single(nil)) + + let (membersDisposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(engine: context.engine, accountPeerId: context.account.peerId, peerId: peer.id) { membersState in + if case .loading = membersState.loadingState, membersState.list.isEmpty { + adminsPromise.set(.single(nil)) + } else { + adminsPromise.set(.single(membersState.list)) + } + } + actionsDisposable.add(membersDisposable) + + let antiSpamBotConfiguration = AntiSpamBotConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let antiSpamBotPeerPromise = Promise(nil) + if let antiSpamBotId = antiSpamBotConfiguration.antiSpamBotId { + antiSpamBotPeerPromise.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: antiSpamBotId)) + |> map { peer in + if let peer = peer, case let .user(user) = peer { + return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer) + } else { + return nil + } + }) + } + + var previousPeers: [RenderedChannelParticipant]? + + let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData + let signal = combineLatest(presentationData, statePromise.get(), adminsPromise.get(), antiSpamBotPeerPromise.get()) + |> deliverOnMainQueue + |> map { presentationData, state, admins, antiSpamBot -> (ItemListControllerState, (ItemListNodeState, Any)) in + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { + dismissImpl?() + }) + + let doneEnabled = !state.events.isEmpty + + let rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: doneEnabled, action: { + var resultState: ChatRecentActionsFilterControllerState? + updateState { current in + resultState = current + return current + } + if let resultState = resultState { + apply(resultState.events, resultState.adminPeerIds) + } + dismissImpl?() + }) + + var sortedAdmins: [RenderedChannelParticipant]? + if let admins = admins { + sortedAdmins = admins.filter { $0.peer.id == context.account.peerId } + admins.filter({ $0.peer.id != context.account.peerId }) + if let antiSpamBot = antiSpamBot { + sortedAdmins?.insert(antiSpamBot, at: 0) + } + } else { + sortedAdmins = nil + } + + let previous = previousPeers + previousPeers = sortedAdmins + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelRecentActionsFilterControllerEntries(presentationData: presentationData, accountPeerId: context.account.peerId, peer: peer, antiSpamBotId: antiSpamBotConfiguration.antiSpamBotId, state: state, participants: sortedAdmins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= sortedAdmins!.count) + + return (controllerState, (listState, arguments)) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(context: context, state: signal) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + return controller +} diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift index 7c68086f5c..a1e5b6d07c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift @@ -152,6 +152,7 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen self.scrollNode.view.alwaysBounceVertical = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0) + self.scrollNode.view.scrollsToTop = false } func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 85ac8110b9..c38f82c1c6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -423,11 +423,11 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, navigateToThreadMessage: { _, _, _ in }, tapMessage: { _ in }, clickThroughMessage: { _, _ in - }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in + }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, editGif: { _, _ in }, requestMessageActionCallback: { _, _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, openExternalInstantPage: { _ in - }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, updatePresentationState: { _ in }, openMessageShareMenu: { _ in + }, shareCurrentLocation: { _ in }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, updatePresentationState: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { @@ -459,7 +459,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in 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/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift index 1aca3bf9a1..61e5a4d228 100644 --- a/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/Chat/EditableTokenListNode/Sources/EditableTokenListNode.swift @@ -229,6 +229,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.scrollNode = ASScrollNode() self.scrollNode.view.alwaysBounceVertical = false self.scrollNode.clipsToBounds = true + self.scrollNode.view.scrollsToTop = false self.placeholderNode = ASTextNode() self.placeholderNode.isUserInteractionEnabled = false @@ -236,6 +237,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.list.itemPlaceholderTextColor) self.textFieldScrollNode = ASScrollNode() + self.textFieldScrollNode.view.scrollsToTop = false self.textFieldNode = TextFieldNode() self.textFieldNode.textField.font = Font.regular(15.0) diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 1387502783..d7b308ff93 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -209,7 +209,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let clickThroughMessage: (UIView?, CGPoint?) -> Void public let toggleMessagesSelection: ([EngineMessage.Id], Bool) -> Void public let sendCurrentMessage: (Bool, ChatSendMessageEffect?) -> Void - public let sendMessage: (String) -> Void + public let sendMessage: (String, EngineMessage.Id?) -> Void public let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?, [EngineItemCollectionId]) -> Bool public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void public let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool @@ -220,8 +220,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let activateSwitchInline: (EnginePeer.Id?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void public let openUrl: (OpenUrl) -> Void public let openExternalInstantPage: (OpenInstantPage) -> Void - public let shareCurrentLocation: () -> Void - public let shareAccountContact: () -> Void + public let shareCurrentLocation: (EngineMessage.Id?) -> Void + public let shareAccountContact: (EngineMessage.Id?) -> Void public let sendBotCommand: (EngineMessage.Id?, String) -> Void public let openInstantPage: (EngineRawMessage, ChatMessageItemAssociatedData?) -> Void public let openWallpaper: (EngineRawMessage) -> Void @@ -264,7 +264,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let displaySwipeToReplyHint: () -> Void public let dismissReplyMarkupMessage: (EngineRawMessage) -> Void public let openMessagePollResults: (EngineMessage.Id, Data) -> Void - public let openPollCreation: (Bool?) -> Void + public let openPollCreation: (EngineMessage.Id?, Bool?) -> Void public let openPollMedia: (EngineRawMessage, PollMediaSubject) -> Void public let displayPollSolution: (TelegramMediaPollResults.Solution?, ASDisplayNode?) -> Void public let displayPsa: (String, ASDisplayNode) -> Void @@ -389,7 +389,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol clickThroughMessage: @escaping (UIView?, CGPoint?) -> Void, toggleMessagesSelection: @escaping ([EngineMessage.Id], Bool) -> Void, sendCurrentMessage: @escaping (Bool, ChatSendMessageEffect?) -> Void, - sendMessage: @escaping (String) -> Void, + sendMessage: @escaping (String, EngineMessage.Id?) -> Void, sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?, [EngineItemCollectionId]) -> Bool, sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void, sendGif: @escaping (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool, @@ -400,8 +400,8 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol activateSwitchInline: @escaping (EnginePeer.Id?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void, openUrl: @escaping (OpenUrl) -> Void, openExternalInstantPage: @escaping (OpenInstantPage) -> Void, - shareCurrentLocation: @escaping () -> Void, - shareAccountContact: @escaping () -> Void, + shareCurrentLocation: @escaping (EngineMessage.Id?) -> Void, + shareAccountContact: @escaping (EngineMessage.Id?) -> Void, sendBotCommand: @escaping (EngineMessage.Id?, String) -> Void, openInstantPage: @escaping (EngineRawMessage, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (EngineRawMessage) -> Void, @@ -444,7 +444,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (EngineRawMessage) -> Void, openMessagePollResults: @escaping (EngineMessage.Id, Data) -> Void, - openPollCreation: @escaping (Bool?) -> Void, + openPollCreation: @escaping (EngineMessage.Id?, Bool?) -> Void, openPollMedia: @escaping (EngineRawMessage, PollMediaSubject) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution?, ASDisplayNode?) -> Void, displayPsa: @escaping (String, ASDisplayNode) -> Void, diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 9491884250..586c7e7219 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -2772,6 +2772,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 }) @@ -3042,7 +3106,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 4f2841b2f4..aad31d517d 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift @@ -427,7 +427,9 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { return false } }, - actionPerformed: nil + actionPerformed: { [weak self] actions in + self?.presentStickerPackActionOverlay(actions) + } ) strongSelf.interaction.presentController(controller, nil) } @@ -528,6 +530,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) @@ -540,7 +609,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { let query = text.trimmingCharacters(in: .whitespacesAndNewlines) let signal: Signal<(StickerPaneSearchStickerState, FoundStickerSets, Bool, FoundStickerSets?)?, NoError> - if query.count >= 2 { + if query.isSingleEmoji || query.count >= 2 { let context = self.context let stickers: Signal if query.isSingleEmoji { 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..2102d5a85a 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,160 @@ 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, + combinedAlignment: true + )) + ), + 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 +1545,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 +1602,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/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift index d23452ebc4..859cc937b3 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift @@ -559,6 +559,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } + self.scrollNode.view.scrollsToTop = false self.backgroundView.contentView.addSubview(self.scrollNode.view) self.scrollNode.addSubnode(self.selectedBackgroundNode) diff --git a/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift b/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift index 717b9b56a1..e0d642a4d7 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift @@ -184,6 +184,8 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { var okButtonLayout: (TextNodeLayout, () -> TextNode)? var cancelButtonLayout: (TextNodeLayout, () -> TextNode)? var alignment: NSTextAlignment = .left + var textLinkAction: (([NSAttributedString.Key: Any], Int) -> Void)? + var textHighlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? switch item.notice { case let .clearStorage(sizeFraction): @@ -269,6 +271,54 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { textString = NSAttributedString(string: item.strings.ChatList_SessionReview_PanelText(newSessionReview.device, newSessionReview.location).string, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) + cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) + case let .reviewBotConnection(newBotConnectionReview, botUsername, totalCount): + spacing = 2.0 + alignment = .center + + var rawTitleString = item.strings.ChatList_BotConnectionReview_PanelTitle + if totalCount > 1 { + rawTitleString = "1/\(totalCount) \(rawTitleString)" + } + titleString = NSAttributedString(string: rawTitleString, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) + + let formattedBotUsername = botUsername.isEmpty ? "" : "[@\(botUsername)](peer)" + let rawText = item.strings.ChatList_BotConnectionReview_PanelText(formattedBotUsername, newBotConnectionReview.device ?? "", newBotConnectionReview.location ?? "").string + textString = parseMarkdownIntoAttributedString(rawText, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor), + bold: MarkdownAttributeSet(font: textBoldFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor), + link: MarkdownAttributeSet(font: textFont, textColor: item.theme.rootController.navigationBar.accentTextColor), + linkAttribute: { _ in + return (TelegramTextAttributes.URL, "peer") + } + )) + textHighlightAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + } + textLinkAction = { [context = item.context] attributes, _ in + guard let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String else { + return + } + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: newBotConnectionReview.botId) + ) + |> deliverOnMainQueue).startStandalone(next: { peer in + guard let peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController { + navigationController.pushViewController(controller) + } else { + context.sharedContext.mainWindow?.present(controller, on: .root) + } + }) + } + okButtonLayout = makeOkButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelConfirm, font: titleFont, textColor: item.theme.list.itemAccentColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) cancelButtonLayout = makeCancelButtonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelReject, font: titleFont, textColor: item.theme.list.itemDestructiveColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0))) case let .starsSubscriptionLowBalance(amount, peers): @@ -331,15 +381,18 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { let _ = titleLayout.1(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: .white, attemptSynchronous: true)) if case .center = alignment { - strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset), size: titleLayout.0.size) + strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - titleLayout.0.size.width) * 0.5), y: verticalInset + 1.0), size: titleLayout.0.size) } else { - strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.0.size) + strongSelf.titleNode.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + 1.0), size: titleLayout.0.size) } let _ = textLayout.1(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: .white, attemptSynchronous: true)) strongSelf.titleNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 1000000.0, height: 1000000.0)) strongSelf.textNode.visibilityRect = CGRect(origin: CGPoint(), size: CGSize(width: 1000000.0, height: 1000000.0)) + strongSelf.textNode.linkHighlightColor = item.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.12) + strongSelf.textNode.highlightAttributeAction = textHighlightAction + strongSelf.textNode.tapAttributeAction = textLinkAction if case .center = alignment { strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: strongSelf.titleNode.textNode.frame.maxY + spacing), size: textLayout.0.size) diff --git a/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift index 4048b8da84..aff49df2a4 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift @@ -55,6 +55,7 @@ public final class ChatListSearchFiltersContainerNode: ASDisplayNode { self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.canCancelContentTouches = true self.scrollNode.view.contentInsetAdjustmentBehavior = .never + self.scrollNode.view.scrollsToTop = false self.view.addSubview(self.backgroundContainer) diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift index e4be5f979c..9c9c5234a3 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift @@ -61,6 +61,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.canCancelContentTouches = true + self.wrappingScrollNode.view.scrollsToTop = false self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift index 58ec1e213a..d459477356 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift @@ -553,7 +553,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { buttonTitle = strings.Conversation_CalendarSearch_Done } - let buttonSideInset: CGFloat = 30.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonSize = self.button.update( transition: transition, component: AnyComponent(ButtonComponent( @@ -583,9 +583,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) - let buttonFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { self.addSubview(buttonView) @@ -632,9 +632,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) - let buttonFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize) if let buttonView = self.secondaryButton.view { if buttonView.superview == nil { self.addSubview(buttonView) @@ -674,9 +674,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) - let buttonFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize) if let buttonView = self.secondaryButton.view { if buttonView.superview == nil { self.addSubview(buttonView) @@ -685,10 +685,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } contentHeight += buttonSize.height } - - let bottomPanelPadding: CGFloat = 15.0 - let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding - contentHeight += bottomInset + contentHeight += buttonInsets.bottom let contentSize = CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/BUILD b/submodules/TelegramUI/Components/ChatThemeScreen/BUILD index fd44f1f568..e056bd9096 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatThemeScreen/BUILD @@ -13,6 +13,11 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/Display", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/SheetComponent", + "//submodules/Components/ViewControllerComponent", "//submodules/TelegramCore", "//submodules/AccountContext", "//submodules/ComponentFlow", @@ -20,13 +25,11 @@ swift_library( "//submodules/TelegramUIPreferences", "//submodules/PresentationDataUtils", "//submodules/TelegramNotices", - "//submodules/AnimationUI", "//submodules/MergeLists", "//submodules/MediaResources", "//submodules/StickerResources", "//submodules/WallpaperResources", "//submodules/TooltipUI", - "//submodules/SolidRoundedButtonNode", "//submodules/AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode", "//submodules/ShimmerEffect", @@ -35,6 +38,9 @@ swift_library( "//submodules/Markdown", "//submodules/AppBundle", "//submodules/ActivityIndicator", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", "//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index 1eab70ac52..6bbd3dad85 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -2,15 +2,16 @@ import Foundation import UIKit import Display import AsyncDisplayKit +import ComponentFlow +import ComponentDisplayAdapters +import ViewControllerComponent import TelegramCore import SwiftSignalKit import AccountContext -import SolidRoundedButtonNode import TelegramPresentationData import TelegramUIPreferences import TelegramNotices import PresentationDataUtils -import AnimationUI import MergeLists import MediaResources import StickerResources @@ -22,6 +23,12 @@ import ShimmerEffect import AttachmentUI import AvatarNode import AlertComponent +import SheetComponent +import ButtonComponent +import GlassBarButtonComponent +import BundleIconComponent +import LottieComponent +import MultilineTextComponent private struct ThemeSettingsThemeEntry: Comparable, Identifiable { let index: Int @@ -216,7 +223,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec if let image = cachedBorderImages[key] { return image } else { - let image = generateImage(CGSize(width: 18.0, height: 18.0), rotatedContext: { size, context in + let image = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -242,7 +249,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec context.setLineWidth(lineWidth) context.strokeEllipse(in: bounds.insetBy(dx: 1.0 + lineWidth / 2.0, dy: 1.0 + lineWidth / 2.0)) } - })?.stretchableImage(withLeftCapWidth: 9, topCapHeight: 9) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 16) cachedBorderImages[key] = image return image } @@ -288,7 +295,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { self.imageNode = TransformImageNode() self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 82.0, height: 108.0)) self.imageNode.isLayerBacked = true - self.imageNode.cornerRadius = 8.0 + self.imageNode.cornerRadius = 16.0 self.imageNode.clipsToBounds = true self.imageNode.contentAnimations = [.subsequentUpdates] @@ -440,7 +447,11 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } } if item.themeReference == nil { - strongSelf.imageNode.backgroundColor = item.theme.list.plainBackgroundColor + if item.theme.overallDarkAppearance { + strongSelf.imageNode.backgroundColor = item.theme.list.plainBackgroundColor + } else { + strongSelf.imageNode.backgroundColor = item.theme.rootController.navigationBar.segmentedForegroundColor + } } if updatedTheme || updatedSelected { @@ -619,8 +630,8 @@ public final class ChatThemeScreen: ViewController { public static let themeCrossfadeDuration: Double = 0.3 public static let themeCrossfadeDelay: Double = 0.25 - private var controllerNode: ChatThemeScreenNode { - return self.displayNode as! ChatThemeScreenNode + private var controllerNode: ChatThemeSheetScreenNode { + return self.displayNode as! ChatThemeSheetScreenNode } private var animatedIn = false @@ -699,7 +710,7 @@ public final class ChatThemeScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = ChatThemeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedTheme: self.initiallySelectedTheme, peerName: self.peerName) + self.displayNode = ChatThemeSheetScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedTheme: self.initiallySelectedTheme, peerName: self.peerName) self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl self.controllerNode.previewTheme = { [weak self] chatTheme, dark in guard let strongSelf = self else { @@ -805,214 +816,418 @@ private func interpolateColors(from: [String: UIColor], to: [String: UIColor], f return colors } -private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelegate { - private let context: AccountContext - private var presentationData: PresentationData - private weak var controller: ChatThemeScreen? +private final class ChatThemeScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment - private let dimNode: ASDisplayNode - private let wrappingScrollNode: ASScrollNode - private let contentContainerNode: ASDisplayNode - private let topContentContainerNode: SparseNode - private let buttonsContentContainerNode: SparseNode - private let effectNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode - private let titleNode: ASTextNode - private let textNode: ImmediateTextNode - private let cancelButtonNode: WebAppCancelButtonNode - private let switchThemeButton: HighlightTrackingButtonNode - private let animationContainerNode: ASDisplayNode - private var animationNode: AnimationNode - private let doneButton: SolidRoundedButtonNode - private let otherButton: HighlightableButtonNode + let context: AccountContext + let presentationData: PresentationData + let animatedEmojiStickers: [String: [StickerPackItem]] + let initiallySelectedTheme: ChatTheme? + let peerName: String + let canResetWallpaper: Bool + let present: (ViewController) -> Void + let presentInRoot: (ViewController) -> Void + let previewTheme: (ChatTheme?, Bool?) -> Void + let changeWallpaper: () -> Void + let resetWallpaper: () -> Void + let completion: (ChatTheme?) -> Void + let cancel: () -> Void - private let listNode: ListView - private var entries: [ThemeSettingsThemeEntry]? - private var enqueuedTransitions: [ThemeSettingsThemeItemNodeTransition] = [] - private var initialized = false - - private let uniqueGiftChatThemesContext: UniqueGiftChatThemesContext - private var currentUniqueGiftChatThemesState: UniqueGiftChatThemesContext.State? - - private let peerName: String - - private let initiallySelectedTheme: ChatTheme? - private var selectedTheme: ChatTheme? { - didSet { - self.selectedThemePromise.set(self.selectedTheme) - } - } - private var selectedThemePromise: ValuePromise - - private var isDarkAppearancePromise: ValuePromise - private var isDarkAppearance: Bool = false { - didSet { - self.isDarkAppearancePromise.set(self.isDarkAppearance) - } - } - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - private let disposable = MetaDisposable() - - var present: ((ViewController) -> Void)? - var previewTheme: ((ChatTheme?, Bool?) -> Void)? - var completion: ((ChatTheme?) -> Void)? - var dismiss: (() -> Void)? - var cancel: (() -> Void)? - - init(context: AccountContext, presentationData: PresentationData, controller: ChatThemeScreen, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedTheme: ChatTheme?, peerName: String) { + init( + context: AccountContext, + presentationData: PresentationData, + animatedEmojiStickers: [String: [StickerPackItem]], + initiallySelectedTheme: ChatTheme?, + peerName: String, + canResetWallpaper: Bool, + present: @escaping (ViewController) -> Void, + presentInRoot: @escaping (ViewController) -> Void, + previewTheme: @escaping (ChatTheme?, Bool?) -> Void, + changeWallpaper: @escaping () -> Void, + resetWallpaper: @escaping () -> Void, + completion: @escaping (ChatTheme?) -> Void, + cancel: @escaping () -> Void + ) { self.context = context - self.controller = controller + self.presentationData = presentationData + self.animatedEmojiStickers = animatedEmojiStickers self.initiallySelectedTheme = initiallySelectedTheme self.peerName = peerName - self.selectedTheme = initiallySelectedTheme - self.selectedThemePromise = ValuePromise(initiallySelectedTheme) - self.presentationData = presentationData + self.canResetWallpaper = canResetWallpaper + self.present = present + self.presentInRoot = presentInRoot + self.previewTheme = previewTheme + self.changeWallpaper = changeWallpaper + self.resetWallpaper = resetWallpaper + self.completion = completion + self.cancel = cancel + } + + static func ==(lhs: ChatThemeScreenComponent, rhs: ChatThemeScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.presentationData.theme !== rhs.presentationData.theme { + return false + } + if lhs.presentationData.strings !== rhs.presentationData.strings { + return false + } + if lhs.initiallySelectedTheme != rhs.initiallySelectedTheme { + return false + } + if lhs.peerName != rhs.peerName { + return false + } + if lhs.canResetWallpaper != rhs.canResetWallpaper { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() - self.uniqueGiftChatThemesContext = UniqueGiftChatThemesContext(account: context.account) + private var component: ChatThemeScreenComponent? + private var environment: ViewControllerComponentContainer.Environment? - self.wrappingScrollNode = ASScrollNode() - self.wrappingScrollNode.view.alwaysBounceVertical = true - self.wrappingScrollNode.view.delaysContentTouches = false - self.wrappingScrollNode.view.canCancelContentTouches = true + override init(frame: CGRect) { + super.init(frame: frame) + } - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = .clear + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.isOpaque = false + func update(component: ChatThemeScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environmentValue + + let sheetEnvironment = SheetComponentEnvironment( + metrics: environmentValue.metrics, + deviceMetrics: environmentValue.deviceMetrics, + isDisplaying: environmentValue.isVisible, + isCentered: false, + hasInputHeight: !environmentValue.inputHeight.isZero, + regularMetricsSize: nil, + dismiss: { _ in + component.cancel() + } + ) + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(ChatThemeSheetContentComponent( + context: component.context, + presentationData: component.presentationData, + animatedEmojiStickers: component.animatedEmojiStickers, + initiallySelectedTheme: component.initiallySelectedTheme, + peerName: component.peerName, + canResetWallpaper: component.canResetWallpaper, + present: component.present, + presentInRoot: component.presentInRoot, + previewTheme: component.previewTheme, + changeWallpaper: component.changeWallpaper, + resetWallpaper: component.resetWallpaper, + completion: component.completion, + cancel: component.cancel + )), + style: .glass, + backgroundColor: .color(environmentValue.theme.actionSheet.opaqueItemBackgroundColor), + clipsContent: true, + isScrollEnabled: false, + hasDimView: false, + animateOut: self.sheetAnimateOut + )), + environment: { + environmentValue + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: sheetSize)) + } + + return availableSize + } - self.topContentContainerNode = SparseNode() - self.topContentContainerNode.isOpaque = false + private func contentView() -> ChatThemeSheetContentComponent.View? { + guard let sheetView = self.sheet.view else { + return nil + } + return findTaggedComponentViewImpl(view: sheetView, tag: ChatThemeSheetContentComponent.Tag()) as? ChatThemeSheetContentComponent.View + } + + func containsContent(point: CGPoint) -> Bool { + guard let contentView = self.contentView() else { + return false + } + return contentView.bounds.contains(self.convert(point, to: contentView)) + } + + func dimTapped() { + self.contentView()?.dimTapped() + } + + func animateOut(completion: @escaping () -> Void) { + self.contentView()?.setAnimatedOut() + self.sheetAnimateOut.invoke(Action { _ in + completion() + }) + } + } + + 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) + } +} - self.buttonsContentContainerNode = SparseNode() - self.buttonsContentContainerNode.isOpaque = false +private final class ChatThemeSheetContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + final class Tag { + } + + let context: AccountContext + let presentationData: PresentationData + let animatedEmojiStickers: [String: [StickerPackItem]] + let initiallySelectedTheme: ChatTheme? + let peerName: String + let canResetWallpaper: Bool + let present: (ViewController) -> Void + let presentInRoot: (ViewController) -> Void + let previewTheme: (ChatTheme?, Bool?) -> Void + let changeWallpaper: () -> Void + let resetWallpaper: () -> Void + let completion: (ChatTheme?) -> Void + let cancel: () -> Void + + init( + context: AccountContext, + presentationData: PresentationData, + animatedEmojiStickers: [String: [StickerPackItem]], + initiallySelectedTheme: ChatTheme?, + peerName: String, + canResetWallpaper: Bool, + present: @escaping (ViewController) -> Void, + presentInRoot: @escaping (ViewController) -> Void, + previewTheme: @escaping (ChatTheme?, Bool?) -> Void, + changeWallpaper: @escaping () -> Void, + resetWallpaper: @escaping () -> Void, + completion: @escaping (ChatTheme?) -> Void, + cancel: @escaping () -> Void + ) { + self.context = context + self.presentationData = presentationData + self.animatedEmojiStickers = animatedEmojiStickers + self.initiallySelectedTheme = initiallySelectedTheme + self.peerName = peerName + self.canResetWallpaper = canResetWallpaper + self.present = present + self.presentInRoot = presentInRoot + self.previewTheme = previewTheme + self.changeWallpaper = changeWallpaper + self.resetWallpaper = resetWallpaper + self.completion = completion + self.cancel = cancel + } + + static func ==(lhs: ChatThemeSheetContentComponent, rhs: ChatThemeSheetContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.presentationData.theme !== rhs.presentationData.theme { + return false + } + if lhs.presentationData.strings !== rhs.presentationData.strings { + return false + } + if lhs.initiallySelectedTheme != rhs.initiallySelectedTheme { + return false + } + if lhs.peerName != rhs.peerName { + return false + } + if lhs.canResetWallpaper != rhs.canResetWallpaper { + return false + } + return true + } + + final class View: UIView, ComponentTaggedView { + private enum PrimaryAction: Equatable { + case chooseWallpaper + case resetTheme + case apply + } - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 + func matches(tag: Any) -> Bool { + return tag is Tag + } - self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance - self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance) + private let title = ComponentView() + private let subtitle = ComponentView() + private let leftButton = ComponentView() + private let switchThemeButton = ComponentView() + private let primaryButton = ComponentView() + private var resetWallpaperButton: ComponentView? + private let switchThemePlayOnce = ActionSlot() - let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - let textColor = self.presentationData.theme.actionSheet.primaryTextColor - let secondaryTextColor = self.presentationData.theme.actionSheet.secondaryTextColor - let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark + private let listNode: ListView + private var entries: [ThemeSettingsThemeEntry]? + private var enqueuedTransitions: [ThemeSettingsThemeItemNodeTransition] = [] + private var initialized = false - self.effectNode = ASDisplayNode(viewBlock: { - return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) - }) + private var component: ChatThemeSheetContentComponent? + private weak var state: EmptyComponentState? + private var environment: EnvironmentType? - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor + private var selectedTheme: ChatTheme? + private var isDarkAppearance: Bool = false + private var isSwitchThemeEnabled = true + private var isCompleting = false - self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_Theme_Title, font: Font.semibold(17.0), textColor: textColor) + private var themes: [TelegramTheme] = [] + private var uniqueGiftChatThemesContext: UniqueGiftChatThemesContext? + private var currentUniqueGiftChatThemesState: UniqueGiftChatThemesContext.State? + private var uniqueGiftPeers: [EnginePeer.Id: EnginePeer] = [:] + private let disposable = MetaDisposable() - self.textNode = ImmediateTextNode() - self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_Theme_Subtitle(peerName).string, font: Font.regular(15.0), textColor: secondaryTextColor) - self.textNode.isHidden = true + private var themeSelectionsCount = 0 + private var displayedPreviewTooltip = false + private var animatedOut = false - self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings) - - self.switchThemeButton = HighlightTrackingButtonNode() - self.animationContainerNode = ASDisplayNode() - self.animationContainerNode.isUserInteractionEnabled = false - - self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) - self.animationNode.isUserInteractionEnabled = false - - self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), glass: true, height: 52.0, cornerRadius: 26.0) - - self.otherButton = HighlightableButtonNode() - - self.listNode = ListViewImpl() - self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) - - super.init() - - self.backgroundColor = nil - self.isOpaque = false - - self.updateButtons() - - self.addSubnode(self.dimNode) - - self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate - self.addSubnode(self.wrappingScrollNode) - - self.wrappingScrollNode.addSubnode(self.backgroundNode) - self.wrappingScrollNode.addSubnode(self.contentContainerNode) - self.wrappingScrollNode.addSubnode(self.topContentContainerNode) - self.wrappingScrollNode.addSubnode(self.buttonsContentContainerNode) - - self.backgroundNode.addSubnode(self.effectNode) - self.backgroundNode.addSubnode(self.contentBackgroundNode) - self.contentContainerNode.addSubnode(self.titleNode) - self.buttonsContentContainerNode.addSubnode(self.textNode) - self.buttonsContentContainerNode.addSubnode(self.doneButton) - self.buttonsContentContainerNode.addSubnode(self.otherButton) - - self.topContentContainerNode.addSubnode(self.animationContainerNode) - self.animationContainerNode.addSubnode(self.animationNode) - self.topContentContainerNode.addSubnode(self.switchThemeButton) - self.topContentContainerNode.addSubnode(self.listNode) - self.topContentContainerNode.addSubnode(self.cancelButtonNode) - - self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) - self.cancelButtonNode.buttonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.doneButton.pressed = { [weak self] in - if let strongSelf = self { - if strongSelf.doneButton.font == .bold { - strongSelf.complete() - } else { - strongSelf.controller?.changeWallpaper() + override init(frame: CGRect) { + self.listNode = ListViewImpl() + self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) + + super.init(frame: frame) + + self.addSubview(self.listNode.view) + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true + + self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in + guard let self, let state = self.currentUniqueGiftChatThemesState, case .ready(true) = state.dataState else { + return + } + if case let .known(value) = offset, value < 100.0 { + self.uniqueGiftChatThemesContext?.loadMore() } } } - self.otherButton.addTarget(self, action: #selector(self.otherButtonPressed), forControlEvents: .touchUpInside) - self.disposable.set(combineLatest( - queue: Queue.mainQueue(), - self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), - self.uniqueGiftChatThemesContext.state - |> mapToSignal { state -> Signal<(UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), NoError> in - var peerIds: [EnginePeer.Id] = [] - for theme in state.themes { - if case let .gift(gift, _) = theme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { - peerIds.append(themePeerId) - } - } - return combineLatest( - .single(state), - context.engine.data.get( - EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)) - ) |> map { peers in - var result: [EnginePeer.Id: EnginePeer] = [:] - for peerId in peerIds { - if let maybePeer = peers[peerId], let peer = maybePeer { - result[peerId] = peer - } - } - return result - } - ) - }, - self.selectedThemePromise.get(), - self.isDarkAppearancePromise.get() - ).startStrict(next: { [weak self] themes, uniqueGiftChatThemesStateAndPeers, selectedTheme, isDarkAppearance in - guard let strongSelf = self else { + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable.dispose() + } + + func setAnimatedOut() { + self.animatedOut = true + } + + private func setupIfNeeded(component: ChatThemeSheetContentComponent) { + guard self.uniqueGiftChatThemesContext == nil else { return } - let (uniqueGiftChatThemesState, peers) = uniqueGiftChatThemesStateAndPeers - strongSelf.currentUniqueGiftChatThemesState = uniqueGiftChatThemesState - - let isFirstTime = strongSelf.entries == nil - let presentationData = strongSelf.presentationData - + + self.selectedTheme = component.initiallySelectedTheme + self.isDarkAppearance = component.presentationData.theme.overallDarkAppearance + + let uniqueGiftChatThemesContext = UniqueGiftChatThemesContext(account: component.context.account) + self.uniqueGiftChatThemesContext = uniqueGiftChatThemesContext + + let context = component.context + self.disposable.set(combineLatest( + queue: Queue.mainQueue(), + context.engine.themes.getChatThemes(accountManager: context.sharedContext.accountManager), + uniqueGiftChatThemesContext.state + |> mapToSignal { state -> Signal<(UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), NoError> in + var peerIds: [EnginePeer.Id] = [] + for theme in state.themes { + if case let .gift(gift, _) = theme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { + peerIds.append(themePeerId) + } + } + return combineLatest( + .single(state), + context.engine.data.get( + EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)) + ) + |> map { peers in + var result: [EnginePeer.Id: EnginePeer] = [:] + for peerId in peerIds { + if let maybePeer = peers[peerId], let peer = maybePeer { + result[peerId] = peer + } + } + return result + } + ) + } + ).startStrict(next: { [weak self] themes, uniqueGiftChatThemesStateAndPeers in + guard let self else { + return + } + self.themes = themes + self.currentUniqueGiftChatThemesState = uniqueGiftChatThemesStateAndPeers.0 + self.uniqueGiftPeers = uniqueGiftChatThemesStateAndPeers.1 + self.rebuildEntries(crossfade: false) + })) + } + + private func hasChanges() -> Bool { + return self.selectedTheme?.id != self.component?.initiallySelectedTheme?.id + } + + private func primaryAction() -> PrimaryAction { + if self.selectedTheme?.id == self.component?.initiallySelectedTheme?.id { + return .chooseWallpaper + } else if self.selectedTheme == nil && self.component?.initiallySelectedTheme != nil { + return .resetTheme + } else { + return .apply + } + } + + private func primaryButtonTitle(strings: PresentationStrings) -> String { + switch self.primaryAction() { + case .chooseWallpaper: + if self.component?.canResetWallpaper == true { + return strings.Conversation_Theme_SetNewPhotoWallpaper + } else { + return strings.Conversation_Theme_SetPhotoWallpaper + } + case .resetTheme: + return strings.Conversation_Theme_Reset + case .apply: + return strings.Conversation_Theme_Apply + } + } + + private func rebuildEntries(crossfade: Bool) { + guard let component = self.component else { + return + } + + let presentationData = component.presentationData + let selectedTheme = self.selectedTheme + let isDarkAppearance = self.isDarkAppearance + var entries: [ThemeSettingsThemeEntry] = [] entries.append(ThemeSettingsThemeEntry( index: 0, @@ -1026,10 +1241,10 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega strings: presentationData.strings, wallpaper: nil )) - - var giftThemes = uniqueGiftChatThemesState.themes + + var giftThemes = self.currentUniqueGiftChatThemesState?.themes ?? [] var existingIds = Set() - if let initiallySelectedTheme, case .gift = initiallySelectedTheme { + if let initiallySelectedTheme = component.initiallySelectedTheme, case .gift = initiallySelectedTheme { let initialThemeIndex = giftThemes.firstIndex(where: { $0.id == initiallySelectedTheme.id }) if initialThemeIndex == nil || initialThemeIndex! > 50 { giftThemes.insert(initiallySelectedTheme, at: 0) @@ -1048,8 +1263,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega emojiFile = file } } - if let themePeerId = uniqueGift.themePeerId, theme.id != initiallySelectedTheme?.id { - peer = peers[themePeerId] + if let themePeerId = uniqueGift.themePeerId, theme.id != component.initiallySelectedTheme?.id { + peer = self.uniqueGiftPeers[themePeerId] } } let themeReference: PresentationThemeReference @@ -1076,15 +1291,16 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega existingIds.insert(theme.id) } - if uniqueGiftChatThemesState.themes.count == 0 || uniqueGiftChatThemesState.dataState == .ready(canLoadMore: false) { - for theme in themes { + let uniqueGiftThemesState = self.currentUniqueGiftChatThemesState + if uniqueGiftThemesState?.themes.count == 0 || uniqueGiftThemesState?.dataState == .ready(canLoadMore: false) { + for theme in self.themes { guard let emoticon = theme.emoticon else { continue } entries.append(ThemeSettingsThemeEntry( index: entries.count, chatTheme: .emoticon(emoticon), - emojiFile: animatedEmojiStickers[emoticon]?.first?.file._parse(), + emojiFile: component.animatedEmojiStickers[emoticon]?.first?.file._parse(), themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)), peer: nil, nightMode: isDarkAppearance, @@ -1095,438 +1311,649 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega )) } } - + let action: (ChatTheme?) -> Void = { [weak self] chatTheme in - if let self, self.selectedTheme != chatTheme { - self.setChatTheme(chatTheme) - } - } - let previousEntries = strongSelf.entries ?? [] - //let crossfade = previousEntries.count != entries.count - let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: false) - strongSelf.enqueueTransition(transition) - - strongSelf.entries = entries - - if isFirstTime { - for theme in themes { - if let wallpaper = theme.settings?.first?.wallpaper, case let .file(file) = wallpaper { - let account = strongSelf.context.account - let accountManager = strongSelf.context.sharedContext.accountManager - let path = accountManager.mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: CachedPreparedPatternWallpaperRepresentation()) - if !FileManager.default.fileExists(atPath: path) { - let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in - let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), complete: false, fetch: true) - - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file.file), reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) - let fetchedFullSizeDisposable = fetchedFullSize.start() - let fullSizeDisposable = accountResource.start(next: { next in - subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) - - if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { - accountManager.mediaBox.storeCachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), data: data) - } - }, error: subscriber.putError, completed: subscriber.putCompletion) - - return ActionDisposable { - fetchedFullSizeDisposable.dispose() - fullSizeDisposable.dispose() - } - } - let _ = accountFullSizeData.start() - } - } - } - } - })) - - self.switchThemeButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.animationContainerNode.layer.removeAnimation(forKey: "opacity") - strongSelf.animationContainerNode.alpha = 0.4 - } else { - strongSelf.animationContainerNode.alpha = 1.0 - strongSelf.animationContainerNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - - self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in - guard let self, let state = self.currentUniqueGiftChatThemesState, case .ready(true) = state.dataState else { - return - } - if case let .known(value) = offset, value < 100.0 { - self.uniqueGiftChatThemesContext.loadMore() - } - } - - self.updateCancelButton() - } - - deinit { - self.disposable.dispose() - } - - private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { - self.enqueuedTransitions.append(transition) - - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - - private func dequeueTransition() { - guard let transition = self.enqueuedTransitions.first else { - return - } - self.enqueuedTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - if self.initialized && transition.crossfade { - options.insert(.AnimateCrossfade) - } - options.insert(.Synchronous) - - var scrollToItem: ListViewScrollToItem? - if !self.initialized { - if let index = transition.entries.firstIndex(where: { entry in - return entry.chatTheme?.id == self.initiallySelectedTheme?.id - }) { - scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) - self.initialized = true - } - } - - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in - }) - } - - private var skipButtonsUpdate = false - private func setChatTheme(_ chatTheme: ChatTheme?) { - self.animateCrossfade(animateIcon: true) - - self.skipButtonsUpdate = true - self.previewTheme?(chatTheme, self.isDarkAppearance) - self.selectedTheme = chatTheme - let _ = ensureThemeVisible(listNode: self.listNode, themeId: chatTheme?.id, animated: true) - - UIView.transition(with: self.buttonsContentContainerNode.view, duration: ChatThemeScreen.themeCrossfadeDuration, options: [.transitionCrossDissolve, .curveLinear]) { - self.updateButtons() - } - self.updateCancelButton() - self.skipButtonsUpdate = false - - self.themeSelectionsCount += 1 - if self.themeSelectionsCount == 2 { - self.maybePresentPreviewTooltip() - } - } - - private func updateButtons() { - let doneButtonTitle: String - var accentButtonTheme = true - var otherIsEnabled = false - if self.selectedTheme?.id == self.initiallySelectedTheme?.id { - otherIsEnabled = self.controller?.canResetWallpaper == true - doneButtonTitle = otherIsEnabled ? self.presentationData.strings.Conversation_Theme_SetNewPhotoWallpaper : self.presentationData.strings.Conversation_Theme_SetPhotoWallpaper - accentButtonTheme = false - } else if self.selectedTheme?.id == nil && self.initiallySelectedTheme?.id != nil { - doneButtonTitle = self.presentationData.strings.Conversation_Theme_Reset - } else { - doneButtonTitle = self.presentationData.strings.Conversation_Theme_Apply - } - - let buttonTheme: SolidRoundedButtonTheme - if accentButtonTheme { - buttonTheme = SolidRoundedButtonTheme(theme: self.presentationData.theme) - } else { - buttonTheme = SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor) - } - UIView.performWithoutAnimation { - self.doneButton.title = doneButtonTitle - self.doneButton.font = accentButtonTheme ? .bold : .regular - } - self.doneButton.updateTheme(buttonTheme) - - self.otherButton.setTitle(self.presentationData.strings.Conversation_Theme_ResetWallpaper, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.destructiveActionTextColor, for: .normal) - self.otherButton.isHidden = !otherIsEnabled - self.textNode.isHidden = !accentButtonTheme || self.controller?.canResetWallpaper == false - - if let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - } - - private func updateCancelButton() { - var cancelButtonState: WebAppCancelButtonNode.State = .cancel - if self.selectedTheme?.id == self.initiallySelectedTheme?.id { - - } else if self.selectedTheme == nil && self.initiallySelectedTheme != nil { - cancelButtonState = .back - } else { - cancelButtonState = .back - } - self.cancelButtonNode.setState(cancelButtonState, animated: true) - } - - private var switchThemeIconAnimator: DisplayLinkAnimator? - func updatePresentationData(_ presentationData: PresentationData) { - guard !self.animatedOut else { - return - } - let previousTheme = self.presentationData.theme - self.presentationData = presentationData - - self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.semibold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor) - - if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - if let animatingCrossFade = self.animatingCrossFade { - Queue.mainQueue().after(!animatingCrossFade ? ChatThemeScreen.themeCrossfadeDelay * UIView.animationDurationFactor() : 0.0, { - self.cancelButtonNode.setTheme(presentationData.theme, animated: true) - }) - } else { - self.cancelButtonNode.setTheme(presentationData.theme, animated: false) - } - - let previousIconColors = iconColors(theme: previousTheme) - let newIconColors = iconColors(theme: self.presentationData.theme) - - if !self.switchThemeButton.isUserInteractionEnabled { - Queue.mainQueue().after(ChatThemeScreen.themeCrossfadeDelay * UIView.animationDurationFactor()) { - self.switchThemeIconAnimator = DisplayLinkAnimator(duration: ChatThemeScreen.themeCrossfadeDuration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in - self?.animationNode.setColors(colors: interpolateColors(from: previousIconColors, to: newIconColors, fraction: value)) - }, completion: { [weak self] in - self?.switchThemeIconAnimator?.invalidate() - self?.switchThemeIconAnimator = nil - }) - - UIView.transition(with: self.buttonsContentContainerNode.view, duration: ChatThemeScreen.themeCrossfadeDuration, options: [.transitionCrossDissolve, .curveLinear]) { - self.updateButtons() - } - } - } else { - self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: newIconColors) - if !self.skipButtonsUpdate { - self.updateButtons() - } - } - } - - override func didLoad() { - super.didLoad() - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never - } - - self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true - } - - @objc func cancelButtonPressed() { - if self.cancelButtonNode.state == .back { - self.setChatTheme(self.initiallySelectedTheme) - } else { - self.cancel?() - } - } - - @objc func otherButtonPressed() { - if self.selectedTheme?.id != self.initiallySelectedTheme?.id { - self.setChatTheme(self.initiallySelectedTheme) - } else { - if self.controller?.canResetWallpaper == true { - self.controller?.resetWallpaper() - self.cancelButtonPressed() - } else { - self.cancelButtonPressed() - } - } - } - - func complete() { - let proceed = { - self.doneButton.isUserInteractionEnabled = false - self.completion?(self.selectedTheme) - } - if case let .gift(gift, _) = self.selectedTheme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: themePeerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { + guard let self, self.selectedTheme != chatTheme else { return } - let controller = giftThemeTransferAlertController( - context: self.context, - gift: uniqueGift, - previousPeer: peer, - commit: { - proceed() + self.setChatTheme(chatTheme) + } + let previousEntries = self.entries ?? [] + let transition = preparedTransition(context: component.context, action: action, from: previousEntries, to: entries, crossfade: crossfade) + self.enqueueTransition(transition) + + let isFirstTime = self.entries == nil + self.entries = entries + + if isFirstTime { + self.preloadWallpaperResources(themes: self.themes, context: component.context) + } + } + + private func preloadWallpaperResources(themes: [TelegramTheme], context: AccountContext) { + for theme in themes { + if let wallpaper = theme.settings?.first?.wallpaper, case let .file(file) = wallpaper { + let account = context.account + let accountManager = context.sharedContext.accountManager + let path = accountManager.mediaBox.cachedRepresentationCompletePath(file.file.resource.id, representation: CachedPreparedPatternWallpaperRepresentation()) + if !FileManager.default.fileExists(atPath: path) { + let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in + let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), complete: false, fetch: true) + + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file.file), reference: .media(media: .standalone(media: file.file), resource: file.file.resource)) + let fetchedFullSizeDisposable = fetchedFullSize.start() + let fullSizeDisposable = accountResource.start(next: { next in + subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + + if next.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedRead) { + accountManager.mediaBox.storeCachedResourceRepresentation(file.file.resource, representation: CachedPreparedPatternWallpaperRepresentation(), data: data) + } + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + let _ = accountFullSizeData.start() } - ) - self.controller?.present(controller, in: .window(.root)) - }) - } else { - proceed() - } - } - - func dimTapped() { - if self.selectedTheme?.id == self.initiallySelectedTheme?.id { - self.cancelButtonPressed() - } else { - let alertController = textAlertController(context: self.context, updatedPresentationData: (self.presentationData, .single(self.presentationData)), title: nil, text: self.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in - if let self { - self.complete() - } - })], actionLayout: .horizontal, dismissOnOutsideTap: true) - self.present?(alertController) - } - } - - @objc func switchThemePressed() { - self.switchThemeButton.isUserInteractionEnabled = false - Queue.mainQueue().after(0.5) { - self.switchThemeButton.isUserInteractionEnabled = true - } - - self.animateCrossfade(animateIcon: false) - self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) - Queue.mainQueue().justDispatch { - self.animationNode.playOnce() - } - - let isDarkAppearance = !self.isDarkAppearance - self.previewTheme?(self.selectedTheme, isDarkAppearance) - self.isDarkAppearance = isDarkAppearance - - if isDarkAppearance { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() - } else { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() - } - } - - private var animatingCrossFade: Bool? - private func animateCrossfade(animateIcon: Bool) { - if animateIcon, let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.animationNode.frame - self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.view) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - - self.animatingCrossFade = animateIcon - Queue.mainQueue().after(ChatThemeScreen.themeCrossfadeDelay * UIView.animationDurationFactor()) { - if let effectView = self.effectNode.view as? UIVisualEffectView { - UIView.animate(withDuration: ChatThemeScreen.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) { - effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } completion: { _ in } } - - let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatThemeScreen.themeCrossfadeDuration) - - self.animatingCrossFade = nil - } - - if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.contentContainerNode.frame - self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.view) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) } - if !animateIcon, let snapshotView = self.otherButton.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.otherButton.frame - self.otherButton.view.superview?.insertSubview(snapshotView, aboveSubview: self.otherButton.view) + private func enqueueTransition(_ transition: ThemeSettingsThemeItemNodeTransition) { + self.enqueuedTransitions.append(transition) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - } - - self.listNode.forEachVisibleItemNode { node in - if let node = node as? ThemeSettingsThemeItemIconNode { - node.crossfade() + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() } } - } - - private var animatedOut = false - func animateIn() { - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - let targetBounds = self.bounds - self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) - self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) - transition.animateView({ - self.bounds = targetBounds - self.dimNode.position = dimPosition - }) - } - - private var themeSelectionsCount = 0 - private var displayedPreviewTooltip = false - private func maybePresentPreviewTooltip() { - guard !self.displayedPreviewTooltip, !self.animatedOut else { - return + private func dequeueTransition() { + guard let transition = self.enqueuedTransitions.first else { + return + } + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if self.initialized && transition.crossfade { + options.insert(.AnimateCrossfade) + } + options.insert(.Synchronous) + + var scrollToItem: ListViewScrollToItem? + if !self.initialized { + if let index = transition.entries.firstIndex(where: { entry in + return entry.chatTheme?.id == self.component?.initiallySelectedTheme?.id + }) { + scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-57.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + self.initialized = true + } + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in + }) } - let frame = self.switchThemeButton.view.convert(self.switchThemeButton.bounds, to: self.view) - let currentTimestamp = Int32(Date().timeIntervalSince1970) - - let isDark = self.presentationData.theme.overallDarkAppearance - - let signal: Signal<(Int32, Int32), NoError> - if isDark { - signal = ApplicationSpecificNotice.getChatSpecificThemeLightPreviewTip(accountManager: self.context.sharedContext.accountManager) - } else { - signal = ApplicationSpecificNotice.getChatSpecificThemeDarkPreviewTip(accountManager: self.context.sharedContext.accountManager) + private func setChatTheme(_ chatTheme: ChatTheme?) { + guard let component = self.component else { + return + } + + self.previewThemeChanged(chatTheme: chatTheme, dark: self.isDarkAppearance) + self.selectedTheme = chatTheme + self.rebuildEntries(crossfade: false) + let _ = ensureThemeVisible(listNode: self.listNode, themeId: chatTheme?.id, animated: true) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: ChatThemeScreen.themeCrossfadeDuration, curve: .easeInOut))) + + self.themeSelectionsCount += 1 + if self.themeSelectionsCount == 2 { + self.maybePresentPreviewTooltip(component: component) + } } - let _ = (signal - |> deliverOnMainQueue).startStandalone(next: { [weak self] count, timestamp in - if let strongSelf = self, count < 2 && currentTimestamp > timestamp + 24 * 60 * 60 { - strongSelf.displayedPreviewTooltip = true + private func previewThemeChanged(chatTheme: ChatTheme?, dark: Bool?) { + self.component?.previewTheme(chatTheme, dark) + self.listNode.forEachVisibleItemNode { node in + if let node = node as? ThemeSettingsThemeItemIconNode { + node.crossfade() + } + } + } + + private func closeOrBackPressed() { + if self.hasChanges() { + self.setChatTheme(self.component?.initiallySelectedTheme) + } else { + self.component?.cancel() + } + } + + private func resetWallpaperPressed() { + self.component?.resetWallpaper() + self.closeOrBackPressed() + } + + private func primaryPressed() { + switch self.primaryAction() { + case .chooseWallpaper: + self.component?.changeWallpaper() + case .resetTheme, .apply: + self.complete() + } + } + + private func complete() { + guard let component = self.component else { + return + } + let proceed = { [weak self] in + guard let self else { + return + } + self.isCompleting = true + self.state?.updated(transition: .immediate) + component.completion(self.selectedTheme) + } + if case let .gift(gift, _) = self.selectedTheme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: themePeerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let controller = giftThemeTransferAlertController( + context: component.context, + gift: uniqueGift, + previousPeer: peer, + commit: { + proceed() + } + ) + self.component?.presentInRoot(controller) + }) + } else { + proceed() + } + } + + func dimTapped() { + guard let component = self.component else { + return + } + if !self.hasChanges() { + self.closeOrBackPressed() + } else { + let alertController = textAlertController(context: component.context, updatedPresentationData: (component.presentationData, .single(component.presentationData)), title: nil, text: component.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: component.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: component.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in + self?.complete() + })], actionLayout: .horizontal, dismissOnOutsideTap: true) + component.present(alertController) + } + } + + private func switchThemePressed() { + guard self.isSwitchThemeEnabled, let component = self.component else { + return + } + self.isSwitchThemeEnabled = false + Queue.mainQueue().after(0.5) { [weak self] in + guard let self else { + return + } + self.isSwitchThemeEnabled = true + self.state?.updated(transition: .immediate) + } + + let isDarkAppearance = !self.isDarkAppearance + self.isDarkAppearance = isDarkAppearance + component.previewTheme(self.selectedTheme, isDarkAppearance) + self.rebuildEntries(crossfade: false) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: ChatThemeScreen.themeCrossfadeDuration, curve: .easeInOut))) + Queue.mainQueue().justDispatch { [weak self] in + self?.switchThemePlayOnce.invoke(Void()) + } + + if isDarkAppearance { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: component.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() + } else { + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: component.context.sharedContext.accountManager, count: 3, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() + } + } + + private func maybePresentPreviewTooltip(component: ChatThemeSheetContentComponent) { + guard !self.displayedPreviewTooltip, !self.animatedOut, let switchThemeButtonView = self.switchThemeButton.view else { + return + } + + let frame = switchThemeButtonView.convert(switchThemeButtonView.bounds, to: self) + let currentTimestamp = Int32(Date().timeIntervalSince1970) + let isDark = component.presentationData.theme.overallDarkAppearance + + let signal: Signal<(Int32, Int32), NoError> + if isDark { + signal = ApplicationSpecificNotice.getChatSpecificThemeLightPreviewTip(accountManager: component.context.sharedContext.accountManager) + } else { + signal = ApplicationSpecificNotice.getChatSpecificThemeDarkPreviewTip(accountManager: component.context.sharedContext.accountManager) + } + + let _ = (signal + |> deliverOnMainQueue).startStandalone(next: { [weak self] count, timestamp in + guard let self, count < 2 && currentTimestamp > timestamp + 24 * 60 * 60 else { + return + } + self.displayedPreviewTooltip = true - strongSelf.present?(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: .plain(text: isDark ? strongSelf.presentationData.strings.Conversation_Theme_PreviewLightShort : strongSelf.presentationData.strings.Conversation_Theme_PreviewDarkShort), style: .default, icon: nil, location: .point(frame.offsetBy(dx: 3.0, dy: 6.0), .bottom), displayDuration: .custom(3.0), inset: 3.0, shouldDismissOnTouch: { _, _ in + component.present(TooltipScreen(account: component.context.account, sharedContext: component.context.sharedContext, text: .plain(text: isDark ? component.presentationData.strings.Conversation_Theme_PreviewLightShort : component.presentationData.strings.Conversation_Theme_PreviewDarkShort), style: .default, icon: nil, location: .point(frame.offsetBy(dx: 3.0, dy: 6.0), .bottom), displayDuration: .custom(3.0), inset: 3.0, shouldDismissOnTouch: { _, _ in return .dismiss(consume: false) })) if isDark { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).startStandalone() + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeLightPreviewTip(accountManager: component.context.sharedContext.accountManager, timestamp: currentTimestamp).startStandalone() } else { - let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).startStandalone() + let _ = ApplicationSpecificNotice.incrementChatSpecificThemeDarkPreviewTip(accountManager: component.context.sharedContext.accountManager, timestamp: currentTimestamp).startStandalone() + } + }) + } + + func update(component: ChatThemeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state + let environmentValue = environment[EnvironmentType.self].value + self.environment = environmentValue + + self.setupIfNeeded(component: component) + + if let previousComponent, previousComponent.presentationData.theme !== component.presentationData.theme || previousComponent.presentationData.strings !== component.presentationData.strings { + self.rebuildEntries(crossfade: false) + } + + let width = availableSize.width + let topInset: CGFloat = 16.0 + let titleBarHeight: CGFloat = 44.0 + let sideInset: CGFloat = 16.0 + let buttonHeight: CGFloat = 52.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environmentValue.safeInsets.bottom, innerDiameter: buttonHeight, sideInset: 30.0) + let bottomInset: CGFloat = environmentValue.safeInsets.bottom.isZero ? 24.0 : environmentValue.safeInsets.bottom + 10.0 + + var contentHeight = topInset + + let leftButtonIconName = self.hasChanges() ? "Navigation/Back" : "Navigation/Close" + let leftButtonSize = self.leftButton.update( + transition: transition, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: component.presentationData.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: leftButtonIconName, component: AnyComponent( + BundleIconComponent( + name: leftButtonIconName, + tintColor: component.presentationData.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak self] _ in + self?.closeOrBackPressed() + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + if let leftButtonView = self.leftButton.view { + if leftButtonView.superview == nil { + self.addSubview(leftButtonView) + } + transition.setFrame(view: leftButtonView, frame: CGRect(origin: CGPoint(x: sideInset, y: topInset), size: leftButtonSize)) + } + + let switchButtonSize = self.switchThemeButton.update( + transition: transition, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: component.presentationData.theme.overallDarkAppearance, + state: .glass, + isEnabled: self.isSwitchThemeEnabled, + component: AnyComponentWithIdentity(id: self.isDarkAppearance ? "night" : "day", component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun"), + color: component.presentationData.theme.chat.inputPanel.panelControlColor, + startingPosition: .end, + size: CGSize(width: 28.0, height: 28.0), + playOnce: self.switchThemePlayOnce + ) + )), + action: { [weak self] _ in + self?.switchThemePressed() + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + if let switchButtonView = self.switchThemeButton.view { + if switchButtonView.superview == nil { + self.addSubview(switchButtonView) + } + transition.setFrame(view: switchButtonView, frame: CGRect(origin: CGPoint(x: width - sideInset - switchButtonSize.width, y: topInset), size: switchButtonSize)) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(Text( + text: component.presentationData.strings.Conversation_Theme_Title, + font: Font.semibold(17.0), + color: component.presentationData.theme.actionSheet.primaryTextColor + )), + environment: {}, + containerSize: CGSize(width: width - 120.0, height: titleBarHeight) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) * 0.5), y: topInset + floorToScreenPixels((titleBarHeight - titleSize.height) * 0.5)), size: titleSize)) + } + + contentHeight += titleBarHeight + 8.0 + + let listHeight: CGFloat = 120.0 + let listFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: listHeight)) + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: listHeight, height: width) + self.listNode.position = CGPoint(x: listFrame.midX, y: listFrame.midY) + var listInsets = UIEdgeInsets() + listInsets.top += environmentValue.safeInsets.left + 12.0 + listInsets.bottom += environmentValue.safeInsets.right + 12.0 + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: listHeight, height: width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + contentHeight += listHeight + 12.0 + + let showSubtitle = self.primaryAction() != .chooseWallpaper && component.canResetWallpaper + if showSubtitle { + let subtitleSize = self.subtitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.presentationData.strings.Conversation_Theme_Subtitle(component.peerName).string, font: Font.regular(15.0), textColor: component.presentationData.theme.actionSheet.secondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: width - 90.0, height: 100.0) + ) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + transition.setAlpha(view: subtitleView, alpha: 1.0) + transition.setFrame(view: subtitleView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize)) + } + contentHeight += subtitleSize.height + 12.0 + } else if let subtitleView = self.subtitle.view { + transition.setAlpha(view: subtitleView, alpha: 0.0) + } + + let primaryAction = self.primaryAction() + let accentColor = component.presentationData.theme.actionSheet.controlAccentColor + let primaryTextColor: UIColor + let primaryBackground: ButtonComponent.Background + switch primaryAction { + case .chooseWallpaper: + primaryTextColor = accentColor + primaryBackground = ButtonComponent.Background( + style: .glass, + color: accentColor.withMultipliedAlpha(0.1), + foreground: accentColor, + pressedColor: accentColor.withMultipliedAlpha(0.2), + cornerRadius: 26.0 + ) + case .resetTheme, .apply: + primaryTextColor = component.presentationData.theme.list.itemCheckColors.foregroundColor + primaryBackground = ButtonComponent.Background( + style: .glass, + color: component.presentationData.theme.list.itemCheckColors.fillColor, + foreground: component.presentationData.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 26.0 + ) + } + + let primaryButtonSize = self.primaryButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: primaryBackground, + content: AnyComponentWithIdentity( + id: AnyHashable(self.primaryButtonTitle(strings: component.presentationData.strings)), + component: AnyComponent(ButtonTextContentComponent( + text: self.primaryButtonTitle(strings: component.presentationData.strings), + badge: 0, + textColor: primaryTextColor, + badgeBackground: primaryTextColor, + badgeForeground: component.presentationData.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: !self.isCompleting, + displaysProgress: self.isCompleting, + action: { [weak self] in + self?.primaryPressed() + } + )), + environment: {}, + containerSize: CGSize(width: width - buttonInsets.left - buttonInsets.right, height: buttonHeight) + ) + if let primaryButtonView = self.primaryButton.view { + if primaryButtonView.superview == nil { + self.addSubview(primaryButtonView) + } + transition.setFrame(view: primaryButtonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: primaryButtonSize)) + } + contentHeight += primaryButtonSize.height + + let showResetWallpaper = component.canResetWallpaper && primaryAction == .chooseWallpaper + if showResetWallpaper { + contentHeight += 8.0 + let resetWallpaperButton: ComponentView + if let current = self.resetWallpaperButton { + resetWallpaperButton = current + } else { + resetWallpaperButton = ComponentView() + self.resetWallpaperButton = resetWallpaperButton + } + + let destructiveColor = component.presentationData.theme.actionSheet.destructiveActionTextColor + let resetButtonSize = resetWallpaperButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: destructiveColor.withMultipliedAlpha(0.1), + foreground: destructiveColor, + pressedColor: destructiveColor.withMultipliedAlpha(0.2), + cornerRadius: 22.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable("resetWallpaper"), + component: AnyComponent(ButtonTextContentComponent( + text: component.presentationData.strings.Conversation_Theme_ResetWallpaper, + badge: 0, + textColor: destructiveColor, + badgeBackground: destructiveColor, + badgeForeground: component.presentationData.theme.actionSheet.itemBackgroundColor + )) + ), + action: { [weak self] in + self?.resetWallpaperPressed() + } + )), + environment: {}, + containerSize: CGSize(width: width - buttonInsets.left - buttonInsets.right, height: buttonHeight) + ) + if let resetButtonView = resetWallpaperButton.view { + if resetButtonView.superview == nil { + self.addSubview(resetButtonView) + } + transition.setAlpha(view: resetButtonView, alpha: 1.0) + transition.setFrame(view: resetButtonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: resetButtonSize)) + } + contentHeight += resetButtonSize.height + } else if let resetWallpaperButton = self.resetWallpaperButton { + self.resetWallpaperButton = nil + if let resetButtonView = resetWallpaperButton.view { + transition.setAlpha(view: resetButtonView, alpha: 0.0, completion: { _ in + resetButtonView.removeFromSuperview() + }) } } - }) + + contentHeight += bottomInset + + return CGSize(width: 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 ChatThemeSheetScreenNode: ViewControllerTracingNode { + private let context: AccountContext + private var presentationData: PresentationData + private weak var controller: ChatThemeScreen? + + private let animatedEmojiStickers: [String: [StickerPackItem]] + private let initiallySelectedTheme: ChatTheme? + private let peerName: String + + private let hostView: ComponentHostView + private var containerLayout: (ContainerViewLayout, CGFloat)? + private var isDisplaying = false + private var animatedOut = false + + var present: ((ViewController) -> Void)? + var previewTheme: ((ChatTheme?, Bool?) -> Void)? + var completion: ((ChatTheme?) -> Void)? + var dismiss: (() -> Void)? + var cancel: (() -> Void)? + var passthroughHitTestImpl: ((CGPoint) -> UIView?)? + + init(context: AccountContext, presentationData: PresentationData, controller: ChatThemeScreen, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedTheme: ChatTheme?, peerName: String) { + self.context = context + self.presentationData = presentationData + self.controller = controller + self.animatedEmojiStickers = animatedEmojiStickers + self.initiallySelectedTheme = initiallySelectedTheme + self.peerName = peerName + self.hostView = ComponentHostView() + + super.init() + + self.backgroundColor = nil + self.isOpaque = false + self.view.addSubview(self.hostView) + } + + private func update(transition: ComponentTransition) { + guard let (layout, navigationBarHeight) = self.containerLayout else { + return + } + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: navigationBarHeight, + 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.isDisplaying, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + + let component = ChatThemeScreenComponent( + context: self.context, + presentationData: self.presentationData, + animatedEmojiStickers: self.animatedEmojiStickers, + initiallySelectedTheme: self.initiallySelectedTheme, + peerName: self.peerName, + canResetWallpaper: self.controller?.canResetWallpaper == true, + present: { [weak self] controller in + self?.present?(controller) + }, + presentInRoot: { [weak self] controller in + self?.controller?.present(controller, in: .window(.root)) + }, + previewTheme: { [weak self] chatTheme, dark in + self?.previewTheme?(chatTheme, dark) + }, + changeWallpaper: { [weak self] in + self?.controller?.changeWallpaper() + }, + resetWallpaper: { [weak self] in + self?.controller?.resetWallpaper() + }, + completion: { [weak self] chatTheme in + self?.completion?(chatTheme) + }, + cancel: { [weak self] in + self?.cancel?() + } + ) + + let _ = self.hostView.update( + transition: transition, + component: AnyComponent(component), + environment: { + environment + }, + containerSize: layout.size + ) + transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size)) + } + + func updatePresentationData(_ presentationData: PresentationData) { + guard !self.animatedOut else { + return + } + self.presentationData = presentationData + self.update(transition: .immediate) + } + + func animateIn() { + self.isDisplaying = true + self.update(transition: ComponentTransition(animation: .none).withUserData(ViewControllerComponentContainer.AnimateInTransition())) } func animateOut(completion: (() -> Void)? = nil) { self.animatedOut = true - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.dismiss?() + if let rootView = self.hostView.componentView as? ChatThemeScreenComponent.View { + rootView.animateOut { [weak self] in + self?.dismiss?() completion?() } - }) + } else { + self.dismiss?() + completion?() + } + } + + func dimTapped() { + if let rootView = self.hostView.componentView as? ChatThemeScreenComponent.View { + rootView.dimTapped() + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + self.update(transition: ComponentTransition(transition)) } - var passthroughHitTestImpl: ((CGPoint) -> UIView?)? override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { var presentingAlertController = false self.controller?.forEachController({ c in @@ -1536,108 +1963,13 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega return true }) - if !presentingAlertController && self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { - if let result = self.passthroughHitTestImpl?(point) { - return result - } else { - return nil - } + if !presentingAlertController && self.bounds.contains(point), let rootView = self.hostView.componentView as? ChatThemeScreenComponent.View, !rootView.containsContent(point: point) { + if let result = self.passthroughHitTestImpl?(point) { + return result + } else { + return nil } } return super.hitTest(point, with: event) } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let contentOffset = scrollView.contentOffset - let additionalTopHeight = max(0.0, -contentOffset.y) - - if additionalTopHeight >= 30.0 { - self.cancelButtonPressed() - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.statusBar, .input]) - let cleanInsets = layout.insets(options: [.statusBar]) - insets.top = max(10.0, insets.top) - - let bottomInset: CGFloat = 10.0 + cleanInsets.bottom - let titleHeight: CGFloat = 54.0 - var contentHeight = titleHeight + bottomInset + 168.0 - if self.controller?.canResetWallpaper == true { - contentHeight += 50.0 - } - if cleanInsets.bottom.isZero { - insets.bottom += 14.0 - contentHeight += 14.0 - } - - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - let sideInset = floor((layout.size.width - width) / 2.0) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) - let contentFrame = contentContainerFrame - - var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) - if backgroundFrame.minY < contentFrame.minY { - backgroundFrame.origin.y = contentFrame.minY - } - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 18.0 + UIScreenPixel), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let switchThemeSize = CGSize(width: 44.0, height: 44.0) - let switchThemeFrame = CGRect(origin: CGPoint(x: contentFrame.width - switchThemeSize.width - 3.0, y: 6.0), size: switchThemeSize) - transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) - transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) - transition.updateFrameAsPositionAndBounds(node: self.animationNode, frame: CGRect(origin: .zero, size: self.animationContainerNode.frame.size)) - - let cancelSize = self.cancelButtonNode.calculateSizeThatFits(CGSize(width: layout.size.width, height: 56.0)) - let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: cancelSize) - transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame) - - let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: layout.intrinsicInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) - let buttonInset: CGFloat = 16.0 - let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInsets.left - buttonInsets.right, transition: transition) - var doneY = contentHeight - doneButtonHeight - 2.0 - insets.bottom - if self.controller?.canResetWallpaper == true { - doneY = contentHeight - doneButtonHeight - 52.0 - insets.bottom - } - transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInsets.left, y: doneY, width: contentFrame.width, height: doneButtonHeight)) - - let otherButtonSize = self.otherButton.measure(CGSize(width: contentFrame.width - buttonInset * 2.0, height: .greatestFiniteMagnitude)) - self.otherButton.frame = CGRect(origin: CGPoint(x: floor((contentFrame.width - otherButtonSize.width) / 2.0), y: contentHeight - otherButtonSize.height - insets.bottom - 15.0), size: otherButtonSize) - - let textSize = self.textNode.updateLayout(CGSize(width: width - 90.0, height: titleHeight)) - let textFrame: CGRect - if self.controller?.canResetWallpaper == true { - textFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - textSize.width) / 2.0), y: contentHeight - textSize.height - insets.bottom - 17.0), size: textSize) - } else { - textFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - textSize.width) / 2.0), y: contentHeight - textSize.height - insets.bottom - 15.0), size: textSize) - } - transition.updateFrame(node: self.textNode, frame: textFrame) - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) - transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) - transition.updateFrame(node: self.buttonsContentContainerNode, frame: contentContainerFrame) - - var listInsets = UIEdgeInsets() - listInsets.top += layout.safeInsets.left + 12.0 - listInsets.bottom += layout.safeInsets.right + 12.0 - - let contentSize = CGSize(width: contentFrame.width, height: 120.0) - - self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) - self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight - 4.0) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } } diff --git a/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift b/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift index 91ded2f86e..8f5ba7ba58 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)) @@ -194,53 +204,89 @@ private let autoremoveTimerValues: [Int32] = [ 365 * 24 * 60 * 60 as Int32 ] -private final class ChatTimerSheetContentComponent: Component { - typealias EnvironmentType = ViewControllerComponentContainer.Environment +public final class ChatTimerPickerContentComponent: Component { + public typealias EnvironmentType = Empty - let style: ChatTimerScreenStyle - let mode: ChatTimerScreenMode - let currentTime: Int32? - let dismiss: () -> Void + public final class LeadingAction: Equatable { + public enum Icon { + case close + case back + } - init( - style: ChatTimerScreenStyle, - mode: ChatTimerScreenMode, - currentTime: Int32?, - dismiss: @escaping () -> Void + public let icon: Icon + public let action: () -> Void + + public init( + icon: Icon, + action: @escaping () -> Void + ) { + self.icon = icon + self.action = action + } + + public static func ==(lhs: LeadingAction, rhs: LeadingAction) -> Bool { + return lhs === rhs + } + } + + public let configuration: ChatTimerScreen.Configuration + public let theme: PresentationTheme + public let strings: PresentationStrings + public let dateTimeFormat: PresentationDateTimeFormat + public let safeInsets: UIEdgeInsets + public let leadingAction: LeadingAction? + public let completion: (Int32?) -> Void + + public init( + configuration: ChatTimerScreen.Configuration, + theme: PresentationTheme, + strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, + safeInsets: UIEdgeInsets, + leadingAction: LeadingAction?, + completion: @escaping (Int32?) -> Void ) { - self.style = style - self.mode = mode - self.currentTime = currentTime - self.dismiss = dismiss + self.configuration = configuration + self.theme = theme + self.strings = strings + self.dateTimeFormat = dateTimeFormat + self.safeInsets = safeInsets + self.leadingAction = leadingAction + self.completion = completion } - 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 + public static func ==(lhs: ChatTimerPickerContentComponent, rhs: ChatTimerPickerContentComponent) -> Bool { + return false } - final class View: UIView, UIPickerViewDataSource, UIPickerViewDelegate { - private let closeButton = ComponentView() + public final class View: UIView, UIPickerViewDataSource, UIPickerViewDelegate { + private struct PickerConfigurationSignature: Equatable { + enum Picker: Equatable { + case timeOfDay + case date + case dateTime + case fixedValues(values: [Int32], selectionStrategy: ChatTimerScreen.Configuration.FixedSelectionStrategy) + } + + let picker: Picker + let currentValue: Int32? + let minimumDate: Date? + let maximumDate: Date? + let pickerValueMapping: ChatTimerScreen.Configuration.PickerValueMapping + } + + private let leadingButton = ComponentView() private let title = ComponentView() private let primaryButton = ComponentView() private let secondaryButton = ComponentView() - private var component: ChatTimerSheetContentComponent? - private var environment: EnvironmentType? + private var component: ChatTimerPickerContentComponent? private weak var state: EmptyComponentState? private var pickerView: TimerPickerView? private var isCompleting = false - override init(frame: CGRect) { + public override init(frame: CGRect) { super.init(frame: frame) } @@ -248,113 +294,193 @@ private final class ChatTimerSheetContentComponent: Component { fatalError("init(coder:) has not been implemented") } + private func pickerTextColor(configuration: ChatTimerScreen.Configuration, theme: PresentationTheme) -> UIColor { + if let pickerTextColor = configuration.pickerTextColor { + return pickerTextColor(theme) + } + + switch configuration.style { + case .default: + return theme.actionSheet.primaryTextColor + case .media: + return .white + } + } + + private func pickerConfigurationSignature(_ configuration: ChatTimerScreen.Configuration) -> PickerConfigurationSignature { + let picker: PickerConfigurationSignature.Picker + switch configuration.picker { + case .timeOfDay: + picker = .timeOfDay + case .date: + picker = .date + case .dateTime: + picker = .dateTime + case let .fixedValues(values, selectionStrategy, _): + picker = .fixedValues(values: values, selectionStrategy: selectionStrategy) + } + return PickerConfigurationSignature( + picker: picker, + currentValue: configuration.currentValue, + minimumDate: configuration.minimumDate, + maximumDate: configuration.maximumDate, + pickerValueMapping: configuration.pickerValueMapping + ) + } + + 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, theme: PresentationTheme, strings: PresentationStrings) { 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, theme: theme).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, theme: theme), forKey: "textColor") + pickerView.selectorColor = self.pickerTextColor(configuration: configuration, theme: theme).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(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, theme: theme), forKey: "textColor") + pickerView.selectorColor = self.pickerTextColor(configuration: configuration, theme: theme).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(strings) + pickerView.minimumDate = configuration.minimumDate ?? Date() + pickerView.maximumDate = configuration.maximumDate + if #available(iOS 13.4, *) { + pickerView.preferredDatePickerStyle = .wheels + } + pickerView.setValue(self.pickerTextColor(configuration: configuration, theme: theme), forKey: "textColor") + pickerView.setValue(false, forKey: "highlightsToday") + pickerView.selectorColor = self.pickerTextColor(configuration: configuration, theme: theme).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,161 +490,122 @@ 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 - if let controller = self.environment?.controller() as? ChatTimerScreen { - controller.completion(value) - } - 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 { - let environment = environment[EnvironmentType.self].value - let previousComponent = self.component - let previousEnvironment = self.environment - let themeUpdated: Bool - let stringsUpdated: Bool - if let previousEnvironment { - themeUpdated = previousEnvironment.theme !== environment.theme - stringsUpdated = previousEnvironment.strings !== environment.strings + let transformedValue: Int32? + if let component = self.component { + transformedValue = component.configuration.completionValueTransform(selectedValue) } else { - themeUpdated = false - stringsUpdated = false + transformedValue = nil + } + self.component?.completion(transformedValue) + } + + public func update( + component: ChatTimerPickerContentComponent, + availableSize: CGSize, + state: EmptyComponentState, + environment: Environment, + transition: ComponentTransition + ) -> CGSize { + let previousComponent = self.component + let themeUpdated = previousComponent?.theme !== component.theme + let stringsUpdated = previousComponent?.strings !== component.strings + let pickerConfigurationUpdated: Bool + if let previousConfiguration = previousComponent?.configuration { + pickerConfigurationUpdated = self.pickerConfigurationSignature(previousConfiguration) != self.pickerConfigurationSignature(component.configuration) + } else { + pickerConfigurationUpdated = true } self.component = 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 || themeUpdated || stringsUpdated || pickerConfigurationUpdated { + self.setupPickerView(configuration: component.configuration, theme: component.theme, strings: component.strings) } let titleColor: UIColor - switch component.style { + switch component.configuration.style { case .default: - titleColor = environment.theme.actionSheet.primaryTextColor + titleColor = component.theme.actionSheet.primaryTextColor case .media: titleColor = .white } let barButtonSize = CGSize(width: 44.0, height: 44.0) - let closeButtonSize = self.closeButton.update( - transition: transition, - component: AnyComponent( - GlassBarButtonComponent( - size: barButtonSize, - backgroundColor: nil, - isDark: environment.theme.overallDarkAppearance, - state: .glass, - component: AnyComponentWithIdentity(id: "close", component: AnyComponent( - BundleIconComponent( - name: "Navigation/Close", - tintColor: environment.theme.chat.inputPanel.panelControlColor - ) - )), - action: { [weak self] _ in - self?.component?.dismiss() - } - ) - ), - environment: {}, - containerSize: barButtonSize - ) - if let closeButtonView = self.closeButton.view { - if closeButtonView.superview == nil { - self.addSubview(closeButtonView) + if let leadingAction = component.leadingAction { + let iconName: String + switch leadingAction.icon { + case .close: + iconName = "Navigation/Close" + case .back: + iconName = "Navigation/Back" } - transition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize)) + let leadingButtonSize = self.leadingButton.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: barButtonSize, + backgroundColor: nil, + isDark: component.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: iconName, component: AnyComponent( + BundleIconComponent( + name: iconName, + tintColor: component.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak self] _ in + self?.component?.leadingAction?.action() + } + ) + ), + environment: {}, + containerSize: barButtonSize + ) + if let leadingButtonView = self.leadingButton.view { + if leadingButtonView.superview == nil { + self.addSubview(leadingButtonView) + } + transition.setFrame(view: leadingButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: leadingButtonSize)) + } + } else if let leadingButtonView = self.leadingButton.view, leadingButtonView.superview != nil { + leadingButtonView.removeFromSuperview() } - 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(component.strings) + if let titleText, !titleText.isEmpty { + let titleWidth = availableSize.width - (component.leadingAction != nil ? 120.0 : 60.0) + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + Text(text: titleText, font: Font.semibold(17.0), color: titleColor) + ), + environment: {}, + containerSize: CGSize(width: titleWidth, 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 @@ -530,100 +617,105 @@ private final class ChatTimerSheetContentComponent: Component { contentHeight += pickerHeight contentHeight += 17.0 - let buttonSideInset: CGFloat = 30.0 - let primaryButtonTitle = self.primaryButtonTitle(component: component, environment: environment) + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: component.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let selectedValue = self.selectedValue() + let primaryButtonTitle = component.configuration.primaryActionTitle(component.strings, component.dateTimeFormat, selectedValue) let primaryButtonSize = self.primaryButton.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( style: .glass, - color: environment.theme.list.itemCheckColors.fillColor, - foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity(id: AnyHashable(primaryButtonTitle), component: AnyComponent( - Text(text: primaryButtonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + Text(text: primaryButtonTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor) )), isEnabled: true, displaysProgress: false, action: { [weak self] in - self?.completeWithPickerValue() + self?.complete(selectedValue: self?.selectedValue()) } )), environment: {}, - containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) if let primaryButtonView = self.primaryButton.view { if primaryButtonView.superview == nil { self.addSubview(primaryButtonView) } - transition.setFrame(view: primaryButtonView, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: primaryButtonSize)) + transition.setFrame(view: primaryButtonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: primaryButtonSize)) } 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 = component.theme.actionSheet.controlAccentColor + case .destructive: + foregroundColor = component.theme.list.itemDestructiveColor + } + let secondaryButtonTitle = secondaryAction.title(component.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: {}, - containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) if let secondaryButtonView = self.secondaryButton.view { if secondaryButtonView.superview == nil { self.addSubview(secondaryButtonView) } - transition.setFrame(view: secondaryButtonView, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: secondaryButtonSize)) + transition.setFrame(view: secondaryButtonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: secondaryButtonSize)) } contentHeight += secondaryButtonSize.height } else if let secondaryButtonView = self.secondaryButton.view, secondaryButtonView.superview != nil { secondaryButtonView.removeFromSuperview() } - let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : 15.0 - contentHeight += bottomInset + contentHeight += buttonInsets.bottom return CGSize(width: availableSize.width, height: contentHeight) } - func numberOfComponents(in pickerView: UIPickerView) -> Int { + public func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - guard let component = self.component else { + public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + 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 { + public func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent componentIndex: Int, reusing view: UIView?) -> UIView { + guard let component = self.component else { return UIView() } @@ -633,34 +725,116 @@ private final class ChatTimerSheetContentComponent: Component { } else { itemView = TimerPickerItemView() } - itemView.textColor = self.pickerTextColor(component: component, environment: environment) + itemView.textColor = self.pickerTextColor(configuration: component.configuration, theme: component.theme) - 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 component.configuration.picker { + case let .fixedValues(values, _, formatter): + let value = values[row] + itemView.value = (value, formatter(component.strings, value)) + case .timeOfDay, .date, .dateTime: + break } return itemView } - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { self.state?.updated(transition: .immediate) } } + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ChatTimerSheetContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let configuration: ChatTimerScreen.Configuration + let dismiss: () -> Void + + init( + configuration: ChatTimerScreen.Configuration, + dismiss: @escaping () -> Void + ) { + self.configuration = configuration + self.dismiss = dismiss + } + + static func ==(lhs: ChatTimerSheetContentComponent, rhs: ChatTimerSheetContentComponent) -> Bool { + return lhs.configuration == rhs.configuration + } + + final class View: UIView { + private let content = ComponentView() + + private var component: ChatTimerSheetContentComponent? + private var environment: EnvironmentType? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update( + component: ChatTimerSheetContentComponent, + availableSize: CGSize, + state: EmptyComponentState, + environment: Environment, + transition: ComponentTransition + ) -> CGSize { + let environment = environment[EnvironmentType.self].value + + self.component = component + self.environment = environment + self.state = state + + self.content.parentState = state + let contentSize = self.content.update( + transition: transition, + component: AnyComponent(ChatTimerPickerContentComponent( + configuration: component.configuration, + theme: environment.theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + safeInsets: environment.safeInsets, + leadingAction: ChatTimerPickerContentComponent.LeadingAction( + icon: .close, + action: { [weak self] in + self?.component?.dismiss() + } + ), + completion: { [weak self] value in + guard let self, let controller = self.environment?.controller() as? ChatTimerScreen else { + return + } + controller.completion(value) + self.component?.dismiss() + } + )), + environment: {}, + containerSize: availableSize + ) + if let contentView = self.content.view { + if contentView.superview == nil { + self.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: contentSize)) + } + + return contentSize + } + } + func makeView() -> View { return View(frame: CGRect()) } @@ -673,31 +847,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 +900,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 +911,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 +948,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 +1182,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/ComposePollScreen/BUILD b/submodules/TelegramUI/Components/ComposePollScreen/BUILD index cecd26b01f..c261fedc22 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/BUILD +++ b/submodules/TelegramUI/Components/ComposePollScreen/BUILD @@ -62,6 +62,7 @@ swift_library( "//submodules/ICloudResources", "//submodules/CountrySelectionUI", "//submodules/TelegramUI/Components/ShareWithPeersScreen", + "//submodules/ChatTextLinkEditUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index d200bcaf60..8253c1768c 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -147,6 +147,10 @@ final class ComposePollScreenComponent: Component { self.media = media } + deinit { + self.uploadDisposable?.dispose() + } + var requiresUpload: Bool { if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations), !(largest.resource is CloudPhotoSizeMediaResource) { return true @@ -154,6 +158,9 @@ final class ComposePollScreenComponent: Component { if let file = self.media.media as? TelegramMediaFile, !(file.resource is CloudDocumentMediaResource) { return true } + if let webpage = self.media.media as? TelegramMediaWebpage, webpage.id?.namespace == Namespaces.Media.LocalFile { + return true + } return false } } @@ -920,7 +927,7 @@ final class ComposePollScreenComponent: Component { case .description, .quizAnswer: availableButtons = [.gallery, .file, .location] default: - availableButtons = [.gallery, .sticker, .location] + availableButtons = [.gallery, .sticker, .location, .link] } let pollAttachmentSubject: PollAttachmentSubject @@ -964,7 +971,10 @@ final class ComposePollScreenComponent: Component { guard let component = self.component, media.requiresUpload, media.uploadDisposable == nil else { return } - media.progress = 0.0 + if media.media.media is TelegramMediaWebpage { + } else { + media.progress = 0.0 + } if let image = media.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { media.uploadDisposable = (standaloneUploadedImage( @@ -1040,6 +1050,29 @@ final class ComposePollScreenComponent: Component { } }) } + if let webpage = media.media.media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + media.uploadDisposable = (webpagePreview(account: component.context.account, urls: [content.url]) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self else { + return + } + var transition: ComponentTransition = .immediate + switch result { + case let .result(result): + if let result { + media.media = .standalone(media: result.webpage) + } +// media.uploadDisposable?.dispose() +// media.uploadDisposable = nil + transition = .easeInOut(duration: 0.2) + default: + break + } + if !self.isUpdating { + self.state?.updated(transition: transition) + } + }) + } } private func openAttachMediaContextMenu(subject: MediaAttachSubject) -> Bool { diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift index e6fbfce77d..9ad77ecf2c 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift @@ -15,6 +15,7 @@ import LocationUI import AttachmentFileController import ChatEntityKeyboardInputNode import ICloudResources +import ChatTextLinkEditUI public enum PollAttachmentSubject { case description @@ -232,6 +233,53 @@ public func presentPollAttachmentScreen( controllerCompletion(controller, controller.mediaPickerContext) }) return true + case .link: + attachmentController?.dismiss(animated: true) + + //TODO:localize + let controller = chatTextLinkEditController( + context: context, + updatedPresentationData: updatedPresentationData, + text: "Attach a link to this option.", + link: nil, + preview: true, + apply: { link, webpage in + guard let link else { + return + } + if let webpage { + completion(.standalone(media: webpage)) + return + } + let mediaId = Int64.random(in: Int64.min ... Int64.max) + let webPage = TelegramMediaWebpage( + webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: mediaId), + content: .Loaded(TelegramMediaWebpageLoadedContent( + url: link, + displayUrl: link, + hash: 0, + type: nil, + websiteName: nil, + title: nil, + text: nil, + embedUrl: nil, + embedType: nil, + embedSize: nil, + duration: nil, + author: nil, + isMediaLargeByDefault: nil, + imageIsVideoCover: false, + image: nil, + file: nil, + story: nil, + attributes: [], + instantPage: nil + ))) + completion(.standalone(media: webPage)) + } + ) + present(controller, false) + return false default: return false } diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index 5eb51a3f78..08b1aeff36 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -318,6 +318,32 @@ final class ComposeTodoScreenComponent: Component { } } + private static func limitedAttributedString(_ text: NSAttributedString, characterLimit: Int) -> NSAttributedString { + if text.string.count <= characterLimit { + return text + } + + let string = text.string + let endIndex = string.index(string.startIndex, offsetBy: characterLimit) + let length = NSRange(string.startIndex ..< endIndex, in: string).length + return text.attributedSubstring(from: NSRange(location: 0, length: length)) + } + + private static func attributedTodoItemLines(from text: NSAttributedString, characterLimit: Int) -> [NSAttributedString] { + var result: [NSAttributedString] = [] + + text.string.enumerateSubstrings(in: text.string.startIndex ..< text.string.endIndex, options: [.byLines, .substringNotRequired]) { _, substringRange, _, _ in + let range = NSRange(substringRange, in: text.string) + let line = text.attributedSubstring(from: range) + if line.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return + } + result.append(Self.limitedAttributedString(line, characterLimit: characterLimit)) + } + + return result + } + func validatedInput() -> TelegramMediaTodo? { if self.todoTextInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { return nil @@ -328,19 +354,10 @@ final class ComposeTodoScreenComponent: Component { if todoItem.textInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).count == 0 { continue } - var entities: [MessageTextEntity] = [] - for entity in generateChatInputTextEntities(todoItem.textInputState.text) { - switch entity.type { - case .CustomEmoji: - entities.append(entity) - default: - break - } - } mappedItems.append( TelegramMediaTodo.Item( text: todoItem.textInputState.text.string, - entities: entities, + entities: generateChatInputTextEntities(todoItem.textInputState.text), id: todoItem.id ) ) @@ -350,16 +367,6 @@ final class ComposeTodoScreenComponent: Component { return nil } - var textEntities: [MessageTextEntity] = [] - for entity in generateChatInputTextEntities(self.todoTextInputState.text) { - switch entity.type { - case .CustomEmoji: - textEntities.append(entity) - default: - break - } - } - var flags: TelegramMediaTodo.Flags = [] if self.isCompletableByOthers { flags.insert(.othersCanComplete) @@ -371,7 +378,7 @@ final class ComposeTodoScreenComponent: Component { return TelegramMediaTodo( flags: flags, text: self.todoTextInputState.text.string, - textEntities: textEntities, + textEntities: generateChatInputTextEntities(self.todoTextInputState.text), items: mappedItems ) } @@ -792,6 +799,7 @@ final class ComposeTodoScreenComponent: Component { }, assumeIsEditing: self.inputMediaNodeTargetTag === self.todoTextFieldTag, characterLimit: component.initialData.maxTodoTextLength, + formattingAvailable: true, emptyLineHandling: .allowed, returnKeyAction: { [weak self] in guard let self else { @@ -884,6 +892,7 @@ final class ComposeTodoScreenComponent: Component { hasLeftInset: true, canReorder: isEnabled, canAdd: isEnabled && i != 0 && i < component.initialData.maxTodoItemsCount, + formattingAvailable: true, emptyLineHandling: .notAllowed, returnKeyAction: { [weak self] in guard let self else { @@ -945,23 +954,19 @@ final class ComposeTodoScreenComponent: Component { return } if case let .text(text) = data { - let lines = text.string.components(separatedBy: "\n") + let lines = ComposeTodoScreenComponent.View.attributedTodoItemLines(from: text, characterLimit: component.initialData.maxTodoItemLength) if !lines.isEmpty { self.endEditing(true) var i = 0 for line in lines { - if line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - continue - } - let line = String(line.prefix(component.initialData.maxTodoItemLength)) if i < self.todoItems.count { - self.todoItems[i].resetText = NSAttributedString(string: line) + self.todoItems[i].resetText = line } else { if self.todoItems.count < component.initialData.maxTodoItemsCount { let todoItem = ComposeTodoScreenComponent.TodoItem( id: self.nextTodoItemId ) - todoItem.resetText = NSAttributedString(string: line) + todoItem.resetText = line self.todoItems.append(todoItem) self.nextTodoItemId += 1 } @@ -1294,7 +1299,14 @@ final class ComposeTodoScreenComponent: Component { component: AnyComponent(ListSectionComponent( theme: theme, style: .glass, - header: nil, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_SettingsTitle, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), footer: nil, items: todoSettingsSectionItems )), diff --git a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift index 559ea77179..d74cdfc1cc 100644 --- a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift +++ b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift @@ -251,7 +251,7 @@ final class NewContactScreenComponent: Component { let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment - let theme = environment.theme + let theme = environment.theme.withModalBlocksBackground() let strings = environment.strings var initialCountryCode: Int32? @@ -861,7 +861,7 @@ final class NewContactScreenComponent: Component { let edgeEffectHeight: CGFloat = 66.0 let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) - self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) + self.edgeEffectView.update(content: theme.list.blocksBackgroundColor, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) let titleSize = self.title.update( transition: transition, diff --git a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift index 475a4d4f54..8eac5fce4c 100644 --- a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift +++ b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/PhoneInputItem.swift @@ -246,7 +246,7 @@ final class PhoneInputItemNode: ListViewItemNode, ItemListItemNode { self.phoneInputNode.number = "+\(countryCodeAndId.0)" } - func processNumberChange(_ number: String) -> Bool { + func processNumberChange(_ number: String, notifyUpdated: Bool = true) -> Bool { guard let item = self.item else { return false } @@ -270,7 +270,9 @@ final class PhoneInputItemNode: ListViewItemNode, ItemListItemNode { self.phoneInputNode.mask = nil self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: item.strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) } - item.updated(number, rawMask) + if notifyUpdated { + item.updated(number, rawMask) + } return true } else { @@ -297,7 +299,10 @@ final class PhoneInputItemNode: ListViewItemNode, ItemListItemNode { } func updateCountryCode(code: Int32, name: String, phoneNumber: String? = nil) { - self.phoneInputNode.codeAndNumber = (code, name, phoneNumber ?? self.phoneInputNode.codeAndNumber.2) + let targetNumber = phoneNumber ?? self.phoneInputNode.codeAndNumber.2 + let targetFullNumber = "+\(code)\(targetNumber)" + let _ = self.processNumberChange(targetFullNumber, notifyUpdated: false) + self.phoneInputNode.codeAndNumber = (code, name, targetNumber) let _ = self.processNumberChange(self.phoneInputNode.number) } diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift index 40aa79c5bf..93924c3791 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift @@ -738,6 +738,7 @@ final class ContextActionsContainerNode: ASDisplayNode { if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } + self.scrollNode.view.scrollsToTop = false super.init() diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift index 1bc22b48f5..940e6132f1 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerImpl.swift @@ -152,6 +152,7 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } + self.scrollNode.view.scrollsToTop = false self.contentContainerNode = ContextContentContainerNode() diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD index 89a9fc772e..495a15c0c6 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/BUILD @@ -24,7 +24,6 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Components/PagerComponent:PagerComponent", "//submodules/Components/MultilineTextComponent:MultilineTextComponent", - "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AccountContext:AccountContext", "//submodules/lottie-ios:Lottie", @@ -33,6 +32,8 @@ swift_library( "//submodules/GZip:GZip", "//submodules/TelegramStringFormatting:TelegramStringFormatting", "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TelegramUI/Components/ChatTimerScreen", + "//submodules/Components/SheetComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift index 48e767cecf..d2ea6a7413 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusPreviewScreen.swift @@ -10,8 +10,9 @@ import ComponentDisplayAdapters import MultilineTextComponent import EmojiStatusComponent import TelegramStringFormatting -import SolidRoundedButtonComponent import PresentationDataUtils +import ChatTimerScreen +import SheetComponent protocol ContextMenuItemWithAction: AnyObject { func performAction() -> ContextMenuPerformActionResult @@ -347,173 +348,116 @@ private final class ContextMenuActionsComponent: Component { private final class TimeSelectionControlComponent: Component { let theme: PresentationTheme let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat let bottomInset: CGFloat + let screenCornerRadius: CGFloat let apply: (Int32) -> Void let cancel: () -> Void init( theme: PresentationTheme, strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, bottomInset: CGFloat, + screenCornerRadius: CGFloat, apply: @escaping (Int32) -> Void, cancel: @escaping () -> Void ) { self.theme = theme self.strings = strings + self.dateTimeFormat = dateTimeFormat self.bottomInset = bottomInset + self.screenCornerRadius = screenCornerRadius self.apply = apply self.cancel = cancel } static func ==(lhs: TimeSelectionControlComponent, rhs: TimeSelectionControlComponent) -> Bool { - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.bottomInset != rhs.bottomInset { - return false - } - return true + return false } final class View: UIView { - private let backgroundView: BlurredBackgroundView - private let pickerView: UIDatePicker - private let titleView: ComponentView - private let leftButtonView: ComponentView - private let actionButtonView: ComponentView + private let backgroundView: SheetBackgroundView + private let contentView: ComponentView private var component: TimeSelectionControlComponent? override init(frame: CGRect) { - self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) - self.pickerView = UIDatePicker() - - self.titleView = ComponentView() - self.leftButtonView = ComponentView() - self.actionButtonView = ComponentView() + self.backgroundView = SheetBackgroundView() + self.contentView = ComponentView() super.init(frame: frame) self.addSubview(self.backgroundView) - - self.pickerView.timeZone = TimeZone(secondsFromGMT: 0) - self.pickerView.datePickerMode = .countDownTimer - self.pickerView.datePickerMode = .dateAndTime - if #available(iOS 13.4, *) { - self.pickerView.preferredDatePickerStyle = .wheels - } - self.pickerView.minimumDate = Date(timeIntervalSince1970: Date().timeIntervalSince1970 + Double(TimeZone.current.secondsFromGMT())) - self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) - - self.addSubview(self.pickerView) - self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc private func datePickerUpdated() { - } - func update(component: TimeSelectionControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - if self.component?.theme !== component.theme { - UILabel.setDateLabel(component.theme.list.itemPrimaryTextColor) - - self.pickerView.setValue(component.theme.list.itemPrimaryTextColor, forKey: "textColor") - - self.backgroundView.updateColor(color: component.theme.contextMenu.backgroundColor, transition: .immediate) - } - self.component = component - let topPanelHeight: CGFloat = 54.0 - let pickerSpacing: CGFloat = 10.0 - - let pickerSize = CGSize(width: availableSize.width, height: 216.0) - let pickerFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + pickerSpacing), size: pickerSize) - - let titleSize = self.titleView.update( + let glassInset: CGFloat = availableSize.width < availableSize.height ? 6.0 : 0.0 + let topCornerRadius: CGFloat = 38.0 + let bottomCornerRadius: CGFloat = max(24.0, component.screenCornerRadius) - 2.0 + + self.contentView.parentState = state + let contentSize = self.contentView.update( transition: transition, - component: AnyComponent(Text(text: component.strings.EmojiStatusSetup_SetUntil, font: Font.semibold(17.0), color: component.theme.list.itemPrimaryTextColor)), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 100.0) - ) - if let titleComponentView = self.titleView.view { - if titleComponentView.superview == nil { - self.addSubview(titleComponentView) - } - transition.setFrame(view: titleComponentView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((topPanelHeight - titleSize.height) / 2.0)), size: titleSize)) - } - - let leftButtonSize = self.leftButtonView.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Text( - text: component.strings.Common_Cancel, - font: Font.regular(17.0), - color: component.theme.list.itemAccentColor - )), - action: { [weak self] in - self?.component?.cancel() - } - ).minSize(CGSize(width: 16.0, height: topPanelHeight))), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 100.0) - ) - if let leftButtonComponentView = self.leftButtonView.view { - if leftButtonComponentView.superview == nil { - self.addSubview(leftButtonComponentView) - } - transition.setFrame(view: leftButtonComponentView, frame: CGRect(origin: CGPoint(x: 16.0, y: floor((topPanelHeight - leftButtonSize.height) / 2.0)), size: leftButtonSize)) - } - - let actionButtonSize = self.actionButtonView.update( - transition: transition, - component: AnyComponent(SolidRoundedButtonComponent( - title: component.strings.EmojiStatusSetup_SetUntil, - icon: nil, - theme: SolidRoundedButtonComponent.Theme(theme: component.theme), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 10.0, - gloss: false, - action: { [weak self] in - guard let strongSelf = self, let component = strongSelf.component else { + component: AnyComponent(ChatTimerPickerContentComponent( + configuration: ChatTimerScreen.Configuration( + style: .default, + title: { strings in + strings.EmojiStatusSetup_SetUntil + }, + picker: .dateTime, + currentValue: nil, + minimumDate: Date(), + pickerValueMapping: .rawTimestamp, + primaryActionTitle: { strings, _, _ in + strings.EmojiStatusSetup_SetUntil + } + ), + theme: component.theme, + strings: component.strings, + dateTimeFormat: component.dateTimeFormat, + safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: component.bottomInset, right: 0.0), + leadingAction: ChatTimerPickerContentComponent.LeadingAction( + icon: .back, + action: { [weak self] in + self?.component?.cancel() + } + ), + completion: { [weak self] value in + guard let self, let value else { return } - - let timestamp = Int32(strongSelf.pickerView.date.timeIntervalSince1970 - Double(TimeZone.current.secondsFromGMT())) - component.apply(timestamp) + self.component?.apply(value) } )), environment: {}, - containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - glassInset * 2.0, height: availableSize.height) ) - let actionButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - actionButtonSize.width) / 2.0), y: pickerFrame.maxY + pickerSpacing), size: actionButtonSize) - if let actionButtonComponentView = self.actionButtonView.view { - if actionButtonComponentView.superview == nil { - self.addSubview(actionButtonComponentView) + let panelFrame = CGRect(origin: CGPoint(x: glassInset, y: 0.0), size: contentSize) + + self.backgroundView.update( + size: contentSize, + color: component.theme.actionSheet.opaqueItemBackgroundColor, + topCornerRadius: topCornerRadius, + bottomCornerRadius: bottomCornerRadius, + transition: transition + ) + transition.setFrame(view: self.backgroundView, frame: panelFrame) + + if let contentComponentView = self.contentView.view { + if contentComponentView.superview == nil { + self.addSubview(contentComponentView) } - transition.setFrame(view: actionButtonComponentView, frame: actionButtonFrame) + transition.setFrame(view: contentComponentView, frame: panelFrame) } - - self.pickerView.frame = pickerFrame - - var size = CGSize(width: availableSize.width, height: actionButtonFrame.maxY) - if component.bottomInset.isZero { - size.height += 10.0 - } else { - size.height += max(10.0, component.bottomInset) - } - - self.backgroundView.update(size: size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) - - return size + + return CGSize(width: availableSize.width, height: contentSize.height + glassInset) } } @@ -553,20 +497,26 @@ final class EmojiStatusPreviewScreenComponent: Component { let theme: PresentationTheme let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat let bottomInset: CGFloat + let screenCornerRadius: CGFloat let item: EmojiStatusComponent let dismiss: (StatusResult?) -> Void init( theme: PresentationTheme, strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, bottomInset: CGFloat, + screenCornerRadius: CGFloat, item: EmojiStatusComponent, dismiss: @escaping (StatusResult?) -> Void ) { self.theme = theme self.strings = strings + self.dateTimeFormat = dateTimeFormat self.bottomInset = bottomInset + self.screenCornerRadius = screenCornerRadius self.item = item self.dismiss = dismiss } @@ -578,9 +528,15 @@ final class EmojiStatusPreviewScreenComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } if lhs.bottomInset != rhs.bottomInset { return false } + if lhs.screenCornerRadius != rhs.screenCornerRadius { + return false + } if lhs.item != rhs.item { return false } @@ -694,7 +650,9 @@ final class EmojiStatusPreviewScreenComponent: Component { component: AnyComponent(TimeSelectionControlComponent( theme: component.theme, strings: component.strings, + dateTimeFormat: component.dateTimeFormat, bottomInset: component.bottomInset, + screenCornerRadius: component.screenCornerRadius, apply: { [weak self] timestamp in guard let strongSelf = self, let component = strongSelf.component else { return diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index faa32424eb..72e5c128cd 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -1216,7 +1216,9 @@ public final class EmojiStatusSelectionController: ViewController { component: AnyComponent(EmojiStatusPreviewScreenComponent( theme: self.presentationData.theme, strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, bottomInset: layout.insets(options: []).bottom, + screenCornerRadius: layout.deviceMetrics.screenCornerRadius, item: EmojiStatusComponent( context: self.context, animationCache: self.context.animationCache, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 1c0a044379..d82defe799 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -40,7 +40,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/UndoUI:UndoUI", "//submodules/Components/MultilineTextComponent:MultilineTextComponent", - "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", + "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/LocalizedPeerData:LocalizedPeerData", "//submodules/TelegramNotices:TelegramNotices", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 59b3223227..7e8a44a1a6 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -19,7 +19,8 @@ import StickerResources import AppBundle import UndoUI import AudioToolbox -import SolidRoundedButtonComponent +import ButtonComponent +import LottieComponent import EmojiTextAttachmentView import EmojiStatusComponent import TelegramNotices @@ -3354,23 +3355,40 @@ public final class EmojiPagerContentComponent: Component { gloss = false } + var groupPremiumButtonItems: [AnyComponentWithIdentity] = [] + groupPremiumButtonItems.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(ButtonTextContentComponent( + text: title, + badge: 0, + textColor: foregroundColor, + badgeBackground: foregroundColor, + badgeForeground: backgroundColor + )))) + if let animationName { + groupPremiumButtonItems.append(AnyComponentWithIdentity(id: "animation", component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: animationName), + color: foregroundColor, + startingPosition: .begin, + size: CGSize(width: 30.0, height: 30.0), + loop: true + )))) + } + let groupPremiumButtonSize = groupPremiumButton.update( transition: groupPremiumButtonTransition, - component: AnyComponent(SolidRoundedButtonComponent( - title: title, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: backgroundColor, - backgroundColors: backgroundColors, - foregroundColor: foregroundColor + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: backgroundColor, + foreground: foregroundColor, + pressedColor: backgroundColor.withMultipliedAlpha(0.8), + cornerRadius: groupBorderRadius, + isShimmering: gloss, + gradient: backgroundColors.count > 1 ? ButtonComponent.Background.Gradient(colors: backgroundColors) : nil + ), + content: AnyComponentWithIdentity( + id: AnyHashable("\(title)-\(animationName ?? "")"), + component: AnyComponent(HStack(groupPremiumButtonItems, spacing: 4.0)) ), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: groupBorderRadius, - gloss: gloss, - animationName: animationName, - iconPosition: .right, - iconSpacing: 4.0, action: { [weak self] in guard let strongSelf = self, let component = strongSelf.component else { return diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift index 933d7da6bc..4a240c11c0 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift @@ -248,6 +248,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu self.scrollNode.view.alwaysBounceVertical = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0) + self.scrollNode.view.scrollsToTop = false } func scrollViewDidScroll(_ scrollView: UIScrollView) { diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index 040cc0c9de..39fa7c4fc4 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -270,12 +270,141 @@ public class GlassBackgroundView: UIView { } } + public struct CornerRadii: Equatable { + public let topLeft: CGFloat + public let topRight: CGFloat + public let bottomLeft: CGFloat + public let bottomRight: CGFloat + + public init(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomLeft = bottomLeft + self.bottomRight = bottomRight + } + + public init(radius: CGFloat) { + self.init(topLeft: radius, topRight: radius, bottomLeft: radius, bottomRight: radius) + } + + fileprivate func insetBy(_ value: CGFloat) -> CornerRadii { + return CornerRadii( + topLeft: max(0.0, self.topLeft - value), + topRight: max(0.0, self.topRight - value), + bottomLeft: max(0.0, self.bottomLeft - value), + bottomRight: max(0.0, self.bottomRight - value) + ) + } + + fileprivate var maximum: CGFloat { + return max(max(self.topLeft, self.topRight), max(self.bottomLeft, self.bottomRight)) + } + } + public enum Shape: Equatable { case roundedRect(cornerRadius: CGFloat) + case customRoundedRect(cornerRadii: CornerRadii) + + fileprivate func cornerRadii(for size: CGSize) -> CornerRadii { + switch self { + case let .roundedRect(cornerRadius): + return GlassBackgroundView.clampedCornerRadii(size: size, cornerRadii: CornerRadii(radius: cornerRadius)) + case let .customRoundedRect(cornerRadii): + return GlassBackgroundView.clampedCornerRadii(size: size, cornerRadii: cornerRadii) + } + } + + func maximumCornerRadius(for size: CGSize) -> CGFloat { + return self.cornerRadii(for: size).maximum + } + } + + static func clampedCornerRadii(size: CGSize, cornerRadii: CornerRadii) -> CornerRadii { + let size = CGSize(width: max(0.0, size.width), height: max(0.0, size.height)) + var cornerRadii = CornerRadii( + topLeft: max(0.0, cornerRadii.topLeft), + topRight: max(0.0, cornerRadii.topRight), + bottomLeft: max(0.0, cornerRadii.bottomLeft), + bottomRight: max(0.0, cornerRadii.bottomRight) + ) + + func scaleFor(edgeLength: CGFloat, _ lhs: CGFloat, _ rhs: CGFloat) -> CGFloat { + let sum = lhs + rhs + if sum <= edgeLength || sum.isZero { + return 1.0 + } + return edgeLength / sum + } + + let scale = min( + 1.0, + scaleFor(edgeLength: size.width, cornerRadii.topLeft, cornerRadii.topRight), + scaleFor(edgeLength: size.width, cornerRadii.bottomLeft, cornerRadii.bottomRight), + scaleFor(edgeLength: size.height, cornerRadii.topLeft, cornerRadii.bottomLeft), + scaleFor(edgeLength: size.height, cornerRadii.topRight, cornerRadii.bottomRight) + ) + + if scale < 1.0 { + cornerRadii = CornerRadii( + topLeft: cornerRadii.topLeft * scale, + topRight: cornerRadii.topRight * scale, + bottomLeft: cornerRadii.bottomLeft * scale, + bottomRight: cornerRadii.bottomRight * scale + ) + } + + return cornerRadii + } + + static func generateRoundedRectPath(rect: CGRect, cornerRadii: CornerRadii) -> CGPath { + let cornerRadii = self.clampedCornerRadii(size: rect.size, cornerRadii: cornerRadii) + let path = CGMutablePath() + + func addCorner(tangent1End: CGPoint, tangent2End: CGPoint, radius: CGFloat) { + if radius > CGFloat.ulpOfOne { + path.addArc(tangent1End: tangent1End, tangent2End: tangent2End, radius: radius) + } else { + path.addLine(to: tangent1End) + path.addLine(to: tangent2End) + } + } + + path.move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadii.topLeft)) + addCorner( + tangent1End: CGPoint(x: rect.minX, y: rect.minY), + tangent2End: CGPoint(x: rect.minX + cornerRadii.topLeft, y: rect.minY), + radius: cornerRadii.topLeft + ) + path.addLine(to: CGPoint(x: rect.maxX - cornerRadii.topRight, y: rect.minY)) + addCorner( + tangent1End: CGPoint(x: rect.maxX, y: rect.minY), + tangent2End: CGPoint(x: rect.maxX, y: rect.minY + cornerRadii.topRight), + radius: cornerRadii.topRight + ) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadii.bottomRight)) + addCorner( + tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), + tangent2End: CGPoint(x: rect.maxX - cornerRadii.bottomRight, y: rect.maxY), + radius: cornerRadii.bottomRight + ) + path.addLine(to: CGPoint(x: rect.minX + cornerRadii.bottomLeft, y: rect.maxY)) + addCorner( + tangent1End: CGPoint(x: rect.minX, y: rect.maxY), + tangent2End: CGPoint(x: rect.minX, y: rect.maxY - cornerRadii.bottomLeft), + radius: cornerRadii.bottomLeft + ) + path.closeSubpath() + + return path + } + + static func generateRoundedRectPath(size: CGSize, cornerRadii: CornerRadii) -> CGPath { + return self.generateRoundedRectPath(rect: CGRect(origin: CGPoint(), size: size), cornerRadii: cornerRadii) } private final class ClippingShapeContext { let view: UIView + private var maskLayer: CAShapeLayer? private(set) var shape: Shape? @@ -288,7 +417,33 @@ public class GlassBackgroundView: UIView { switch shape { case let .roundedRect(cornerRadius): + self.maskLayer = nil + self.view.layer.mask = nil transition.setCornerRadius(layer: self.view.layer, cornerRadius: cornerRadius) + case let .customRoundedRect(cornerRadii): + transition.setCornerRadius(layer: self.view.layer, cornerRadius: 0.0) + if #available(iOS 26.0, *) { + transition.animateView { + self.view.cornerConfiguration = .corners( + topLeftRadius: .fixed(cornerRadii.topLeft), + topRightRadius: .fixed(cornerRadii.topRight), + bottomLeftRadius: .fixed(cornerRadii.bottomLeft), + bottomRightRadius: .fixed(cornerRadii.bottomRight) + ) + } + } else { + let maskLayer: CAShapeLayer + if let current = self.maskLayer { + maskLayer = current + } else { + maskLayer = CAShapeLayer() + maskLayer.fillColor = UIColor.black.cgColor + self.maskLayer = maskLayer + self.view.layer.mask = maskLayer + } + transition.setFrame(layer: maskLayer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setShapeLayerPath(layer: maskLayer, path: GlassBackgroundView.generateRoundedRectPath(size: size, cornerRadii: cornerRadii)) + } } } } @@ -311,6 +466,7 @@ public class GlassBackgroundView: UIView { private let legacyView: LegacyGlassView? private let legacyHighlightContainerView: UIView? + private let legacyHighlightClippingContext: ClippingShapeContext? private var glassHighlightRecognizer: GlassHighlightGestureRecognizer? private let nativeView: UIVisualEffectView? @@ -342,6 +498,7 @@ public class GlassBackgroundView: UIView { if #available(iOS 26.0, *), !GlassBackgroundView.useCustomGlassImpl { self.legacyView = nil self.legacyHighlightContainerView = nil + self.legacyHighlightClippingContext = nil let glassEffect = UIGlassEffect(style: .regular) glassEffect.isInteractive = false @@ -362,6 +519,7 @@ public class GlassBackgroundView: UIView { legacyHighlightContainerView.isUserInteractionEnabled = false legacyHighlightContainerView.clipsToBounds = true self.legacyHighlightContainerView = legacyHighlightContainerView + self.legacyHighlightClippingContext = ClippingShapeContext(view: legacyHighlightContainerView) self.nativeView = nil self.nativeViewClippingContext = nil self.nativeParamsView = nil @@ -438,13 +596,21 @@ public class GlassBackgroundView: UIView { public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { let shape: Shape = .roundedRect(cornerRadius: cornerRadius) + self.update(size: size, shape: shape, isDark: isDark, tintColor: tintColor, isInteractive: isInteractive, isVisible: isVisible, transition: transition) + } + + public func update(size: CGSize, cornerRadii: CornerRadii, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { + let shape: Shape = .customRoundedRect(cornerRadii: cornerRadii) + self.update(size: size, shape: shape, isDark: isDark, tintColor: tintColor, isInteractive: isInteractive, isVisible: isVisible, transition: transition) + } + + func update(size: CGSize, shape: Shape, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) { if let glassHighlightRecognizer = self.glassHighlightRecognizer { glassHighlightRecognizer.isEnabled = isInteractive } - if let nativeView = self.nativeView, let nativeViewClippingContext = self.nativeViewClippingContext, (nativeView.bounds.size != size || nativeViewClippingContext.shape != shape) { - + if let nativeView = self.nativeView, let nativeViewClippingContext = self.nativeViewClippingContext, (nativeView.bounds.size != size || nativeViewClippingContext.shape != shape || (nativeView.overrideUserInterfaceStyle == .dark) != isDark) { nativeViewClippingContext.update(shape: shape, size: size, transition: transition) if transition.animation.isImmediate { nativeView.frame = CGRect(origin: CGPoint(), size: size) @@ -457,24 +623,21 @@ public class GlassBackgroundView: UIView { nativeView.overrideUserInterfaceStyle = isDark ? .dark : .light } if let legacyView = self.legacyView { - switch shape { - case let .roundedRect(cornerRadius): - let style: LegacyGlassView.Style - switch tintColor.kind { - case .panel: - style = .normal + let style: LegacyGlassView.Style + switch tintColor.kind { + case .panel: + style = .normal + case .clear: + style = .clear + case let .custom(styleValue, _): + switch styleValue { case .clear: style = .clear - case let .custom(styleValue, _): - switch styleValue { - case .clear: - style = .clear - case .default: - style = .normal - } + case .default: + style = .normal } - legacyView.update(size: size, cornerRadius: cornerRadius, style: style, transition: transition) } + legacyView.update(size: size, shape: shape, style: style, transition: transition) transition.setFrame(view: legacyView, frame: CGRect(origin: CGPoint(), size: size)) transition.setAlpha(view: legacyView, alpha: isVisible ? 1.0 : 0.0) @@ -483,10 +646,7 @@ public class GlassBackgroundView: UIView { } if let legacyHighlightContainerView = self.legacyHighlightContainerView { transition.setFrame(view: legacyHighlightContainerView, frame: CGRect(origin: CGPoint(), size: size)) - switch shape { - case let .roundedRect(cornerRadius): - transition.setCornerRadius(layer: legacyHighlightContainerView.layer, cornerRadius: cornerRadius) - } + self.legacyHighlightClippingContext?.update(shape: shape, size: size, transition: transition) } let shadowInset: CGFloat = 32.0 @@ -534,14 +694,13 @@ public class GlassBackgroundView: UIView { if self.params != params { self.params = params - let outerCornerRadius: CGFloat - switch shape { - case let .roundedRect(cornerRadius): - outerCornerRadius = cornerRadius - } - if let shadowView = self.shadowView { - shadowView.image = Self.generateLegacyShadowImage(cornerRadius: outerCornerRadius, shadowInset: shadowInset) + switch shape { + case let .roundedRect(cornerRadius): + shadowView.image = Self.generateLegacyShadowImage(cornerRadius: cornerRadius, shadowInset: shadowInset) + case let .customRoundedRect(cornerRadii): + shadowView.image = Self.generateLegacyShadowImage(cornerRadii: GlassBackgroundView.clampedCornerRadii(size: size, cornerRadii: cornerRadii), shadowInset: shadowInset) + } transition.setAlpha(view: shadowView, alpha: isVisible ? 1.0 : 0.0) } @@ -568,7 +727,12 @@ public class GlassBackgroundView: UIView { borderWidthFactor = 1.0 } } - foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(size: CGSize(width: outerCornerRadius * 2.0, height: outerCornerRadius * 2.0), inset: shadowInset, borderWidthFactor: borderWidthFactor, isDark: isDark, fillColor: fillColor) + switch shape { + case let .roundedRect(cornerRadius): + foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(size: CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0), inset: shadowInset, borderWidthFactor: borderWidthFactor, isDark: isDark, fillColor: fillColor) + case let .customRoundedRect(cornerRadii): + foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(cornerRadii: GlassBackgroundView.clampedCornerRadii(size: size, cornerRadii: cornerRadii), inset: shadowInset, borderWidthFactor: borderWidthFactor, isDark: isDark, fillColor: fillColor) + } #if DEBUG //foregroundView.image = nil #endif @@ -606,7 +770,7 @@ public class GlassBackgroundView: UIView { glassEffectValue.tintColor = nil } } - glassEffectValue.isInteractive = params.isInteractive + glassEffectValue.isInteractive = isInteractive glassEffect = glassEffectValue } @@ -855,6 +1019,33 @@ private extension CGContext { } } +private struct LegacyResizableImageMetrics { + let imageSize: CGSize + let innerRect: CGRect + let leftCapWidth: Int + let topCapHeight: Int +} + +private func legacyResizableImageMetrics(cornerRadii: GlassBackgroundView.CornerRadii, inset: CGFloat) -> LegacyResizableImageMetrics { + let leftRadius = ceil(max(cornerRadii.topLeft, cornerRadii.bottomLeft)) + let rightRadius = ceil(max(cornerRadii.topRight, cornerRadii.bottomRight)) + let topRadius = ceil(max(cornerRadii.topLeft, cornerRadii.topRight)) + let bottomRadius = ceil(max(cornerRadii.bottomLeft, cornerRadii.bottomRight)) + + let innerSize = CGSize( + width: max(1.0, leftRadius + rightRadius + 1.0), + height: max(1.0, topRadius + bottomRadius + 1.0) + ) + let imageSize = CGSize(width: innerSize.width + inset * 2.0, height: innerSize.height + inset * 2.0) + + return LegacyResizableImageMetrics( + imageSize: imageSize, + innerRect: CGRect(origin: CGPoint(x: inset, y: inset), size: innerSize), + leftCapWidth: Int(ceil(inset + leftRadius)), + topCapHeight: Int(ceil(inset + topRadius)) + ) +} + public extension GlassBackgroundView { static func generateLegacyShadowImage(cornerRadius: CGFloat, shadowInset: CGFloat = 32.0, shadowIntensity: CGFloat = 0.04, shadowBlur: CGFloat = 40.0) -> UIImage? { let shadowInnerInset: CGFloat = 0.5 @@ -884,6 +1075,31 @@ public extension GlassBackgroundView { topCapHeight: Int(shadowInset + cornerRadius) ) } + + static func generateLegacyShadowImage(cornerRadii: CornerRadii, shadowInset: CGFloat = 32.0, shadowIntensity: CGFloat = 0.04, shadowBlur: CGFloat = 40.0) -> UIImage? { + let shadowInnerInset: CGFloat = 0.5 + let metrics = legacyResizableImageMetrics(cornerRadii: cornerRadii, inset: shadowInset) + + return generateImage(metrics.imageSize, rotatedContext: { _, context in + context.clear(CGRect(origin: CGPoint(), size: metrics.imageSize)) + + let shadowRect = metrics.innerRect.insetBy(dx: shadowInnerInset, dy: shadowInnerInset) + let shadowPath = GlassBackgroundView.generateRoundedRectPath(rect: shadowRect, cornerRadii: cornerRadii) + + context.setFillColor(UIColor.black.cgColor) + context.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: shadowBlur, color: UIColor(white: 0.0, alpha: shadowIntensity).cgColor) + context.addPath(shadowPath) + context.fillPath() + + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.addPath(shadowPath) + context.fillPath() + })?.stretchableImage( + withLeftCapWidth: metrics.leftCapWidth, + topCapHeight: metrics.topCapHeight + ) + } static func generateLegacyGlassImage(size: CGSize, inset: CGFloat, borderWidthFactor: CGFloat = 1.0, isDark: Bool, fillColor: UIColor) -> UIImage { var size = size @@ -1078,6 +1294,106 @@ public extension GlassBackgroundView { }.stretchableImage(withLeftCapWidth: Int(size.width * 0.5), topCapHeight: Int(size.height * 0.5)) } + static func generateLegacyGlassImage(cornerRadii: CornerRadii, inset: CGFloat, borderWidthFactor: CGFloat = 1.0, isDark: Bool, fillColor: UIColor) -> UIImage { + let metrics = legacyResizableImageMetrics(cornerRadii: cornerRadii, inset: inset) + let size = metrics.imageSize + let innerRect = metrics.innerRect + + return UIGraphicsImageRenderer(size: size).image { ctx in + let context = ctx.cgContext + + context.clear(CGRect(origin: CGPoint(), size: size)) + + let addShadow: (CGContext, Bool, CGPoint, CGFloat, CGFloat, UIColor, CGBlendMode) -> Void = { context, isOuter, position, blur, spread, shadowColor, blendMode in + var blur = blur + + if isOuter { + blur += abs(spread) + + context.beginTransparencyLayer(auxiliaryInfo: nil) + context.saveGState() + defer { + context.restoreGState() + context.endTransparencyLayer() + } + + let spreadRect = innerRect.insetBy(dx: 0.25, dy: 0.25) + let spreadPath = GlassBackgroundView.generateRoundedRectPath(rect: spreadRect, cornerRadii: cornerRadii) + + context.setShadow(offset: CGSize(width: position.x, height: position.y), blur: blur, color: shadowColor.cgColor) + context.setFillColor(UIColor.black.withAlphaComponent(1.0).cgColor) + context.addPath(spreadPath) + context.fillPath() + + let cleanPath = GlassBackgroundView.generateRoundedRectPath(rect: innerRect, cornerRadii: cornerRadii) + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.addPath(cleanPath) + context.fillPath() + context.setBlendMode(.normal) + } else { + let image = UIGraphicsImageRenderer(size: size).image(actions: { ctx in + let context = ctx.cgContext + let spreadRect = innerRect.insetBy(dx: -spread - 0.33, dy: -spread - 0.33) + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: position.x, height: position.y), blur: blur, color: shadowColor.cgColor) + context.setFillColor(shadowColor.cgColor) + context.addPath(UIBezierPath(rect: spreadRect.insetBy(dx: -10000.0, dy: -10000.0)).cgPath) + context.addPath(GlassBackgroundView.generateRoundedRectPath(rect: spreadRect, cornerRadii: cornerRadii)) + context.fillPath(using: .evenOdd) + }) + + UIGraphicsPushContext(context) + image.draw(in: CGRect(origin: .zero, size: size), blendMode: blendMode, alpha: 1.0) + UIGraphicsPopContext() + } + } + + addShadow(context, true, CGPoint(), 30.0, 0.0, UIColor(white: 0.0, alpha: 0.045), .normal) + addShadow(context, true, CGPoint(), 20.0, 0.0, UIColor(white: 0.0, alpha: 0.01), .normal) + + var a: CGFloat = 0.0 + var b: CGFloat = 0.0 + var s: CGFloat = 0.0 + fillColor.getHue(nil, saturation: &s, brightness: &b, alpha: &a) + + context.setFillColor(fillColor.cgColor) + context.addPath(GlassBackgroundView.generateRoundedRectPath(rect: innerRect, cornerRadii: cornerRadii)) + context.fillPath() + + let lineWidth: CGFloat = (isDark ? 0.8 : 0.8) * borderWidthFactor + let strokeColor: UIColor + let blendMode: CGBlendMode + let baseAlpha: CGFloat = isDark ? 0.3 : 0.6 + + if s == 0.0 && abs(a - 0.7) < 0.1 && !isDark { + blendMode = .normal + strokeColor = UIColor(white: 1.0, alpha: baseAlpha) + } else if s <= 0.3 && !isDark { + blendMode = .normal + strokeColor = UIColor(white: 1.0, alpha: 0.7 * baseAlpha) + } else if b >= 0.2 { + let maxAlpha: CGFloat = isDark ? 0.7 : 0.8 + blendMode = .overlay + strokeColor = UIColor(white: 1.0, alpha: max(0.5, min(1.0, maxAlpha * s)) * baseAlpha) + } else { + blendMode = .normal + strokeColor = UIColor(white: 1.0, alpha: 0.5 * baseAlpha) + } + + context.addPath(GlassBackgroundView.generateRoundedRectPath(rect: innerRect, cornerRadii: cornerRadii)) + context.clip() + context.setBlendMode(blendMode) + context.setLineWidth(lineWidth) + context.setStrokeColor(strokeColor.cgColor) + context.addPath(GlassBackgroundView.generateRoundedRectPath(rect: innerRect.insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadii: cornerRadii.insetBy(lineWidth * 0.5))) + context.strokePath() + context.resetClip() + context.setBlendMode(.normal) + }.stretchableImage(withLeftCapWidth: metrics.leftCapWidth, topCapHeight: metrics.topCapHeight) + } + static func generateForegroundImage(size: CGSize, isDark: Bool, fillColor: UIColor) -> UIImage { var size = size if size == .zero { @@ -1161,7 +1477,7 @@ public extension GlassBackgroundView { public final class GlassBackgroundComponent: Component { private let size: CGSize - private let cornerRadius: CGFloat + private let shape: GlassBackgroundView.Shape private let isDark: Bool private let tintColor: GlassBackgroundView.TintColor private let isInteractive: Bool @@ -1176,7 +1492,23 @@ public final class GlassBackgroundComponent: Component { isVisible: Bool = true ) { self.size = size - self.cornerRadius = cornerRadius + self.shape = .roundedRect(cornerRadius: cornerRadius) + self.isDark = isDark + self.tintColor = tintColor + self.isInteractive = isInteractive + self.isVisible = isVisible + } + + public init( + size: CGSize, + cornerRadii: GlassBackgroundView.CornerRadii, + isDark: Bool, + tintColor: GlassBackgroundView.TintColor, + isInteractive: Bool = false, + isVisible: Bool = true + ) { + self.size = size + self.shape = .customRoundedRect(cornerRadii: cornerRadii) self.isDark = isDark self.tintColor = tintColor self.isInteractive = isInteractive @@ -1187,7 +1519,7 @@ public final class GlassBackgroundComponent: Component { if lhs.size != rhs.size { return false } - if lhs.cornerRadius != rhs.cornerRadius { + if lhs.shape != rhs.shape { return false } if lhs.isDark != rhs.isDark { @@ -1207,7 +1539,7 @@ public final class GlassBackgroundComponent: Component { public final class View: GlassBackgroundView { func update(component: GlassBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.update(size: component.size, cornerRadius: component.cornerRadius, isDark: component.isDark, tintColor: component.tintColor, isInteractive: component.isInteractive, isVisible: component.isVisible, transition: transition) + self.update(size: component.size, shape: component.shape, isDark: component.isDark, tintColor: component.tintColor, isInteractive: component.isInteractive, isVisible: component.isVisible, transition: transition) return component.size } diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift index fd7a918447..7b5261d17c 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/LegacyGlassView.swift @@ -88,17 +88,18 @@ final class LegacyGlassView: UIView { private struct Params: Equatable { let size: CGSize - let cornerRadius: CGFloat + let shape: GlassBackgroundView.Shape let style: Style - init(size: CGSize, cornerRadius: CGFloat, style: Style) { + init(size: CGSize, shape: GlassBackgroundView.Shape, style: Style) { self.size = size - self.cornerRadius = cornerRadius + self.shape = shape self.style = style } } private var params: Params? + private var maskLayer: CAShapeLayer? private let backdropLayer: CALayer? private let backdropLayerDelegate: BackdropLayerDelegate @@ -126,7 +127,11 @@ final class LegacyGlassView: UIView { } func update(size: CGSize, cornerRadius: CGFloat, style: Style, transition: ComponentTransition) { - let params = Params(size: size, cornerRadius: cornerRadius, style: style) + self.update(size: size, shape: .roundedRect(cornerRadius: cornerRadius), style: style, transition: transition) + } + + func update(size: CGSize, shape: GlassBackgroundView.Shape, style: Style, transition: ComponentTransition) { + let params = Params(size: size, shape: shape, style: style) let previousParams = self.params if self.params == params { return @@ -168,12 +173,31 @@ final class LegacyGlassView: UIView { } } - transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius) + switch shape { + case let .roundedRect(cornerRadius): + self.maskLayer = nil + self.layer.mask = nil + transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius) + case let .customRoundedRect(cornerRadii): + transition.setCornerRadius(layer: self.layer, cornerRadius: 0.0) + + let maskLayer: CAShapeLayer + if let current = self.maskLayer { + maskLayer = current + } else { + maskLayer = CAShapeLayer() + maskLayer.fillColor = UIColor.black.cgColor + self.maskLayer = maskLayer + self.layer.mask = maskLayer + } + transition.setFrame(layer: maskLayer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setShapeLayerPath(layer: maskLayer, path: GlassBackgroundView.generateRoundedRectPath(size: size, cornerRadii: cornerRadii)) + } transition.setFrame(layer: backdropLayer, frame: CGRect(origin: CGPoint(), size: size)) if #available(iOS 17.0, *), DeviceMetrics.performance.isGraphicallyCapable { let size = CGSize(width: max(1.0, size.width), height: max(1.0, size.height)) - let cornerRadius = min(min(size.width, size.height) * 0.5, cornerRadius) + let cornerRadius = min(min(size.width, size.height) * 0.5, shape.maximumCornerRadius(for: size)) let displacementMagnitudePoints: CGFloat = 20.0 let displacementMagnitudeU = displacementMagnitudePoints / size.width let displacementMagnitudeV = displacementMagnitudePoints / size.height diff --git a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift index 3efa665988..9d60cd341d 100644 --- a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift +++ b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift @@ -216,6 +216,7 @@ public final class GlassBarButtonComponent: Component { } } componentTransition.setFrame(view: view, frame: componentFrame) + componentTransition.setAlpha(view: view, alpha: component.isVisible ? 1.0 : 0.0) } let effectiveState: DisplayState = component.state ?? .glass diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift index ce0204d62b..531138fe3c 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift @@ -15,7 +15,7 @@ public final class GlassControlGroupComponent: Component { case icon(String) case text(String) case animation(String) - case customIcon(id: AnyHashable, component: AnyComponent) + case customIcon(id: AnyHashable, component: AnyComponent, insets: UIEdgeInsets) enum Id: Hashable { case icon(String) @@ -32,7 +32,7 @@ public final class GlassControlGroupComponent: Component { return .text(text) case let .animation(animation): return .animation(animation) - case let .customIcon(id, _): + case let .customIcon(id, _, _): return .customIcon(id) } } @@ -216,8 +216,10 @@ public final class GlassControlGroupComponent: Component { size: CGSize(width: 32.0, height: 32.0), playOnce: playOnce )) - case let .customIcon(_, customIcon): + case let .customIcon(_, customIcon, insets): content = customIcon + itemInsets.left = insets.left + itemInsets.right = insets.right } var minItemWidth: CGFloat = availableSize.height @@ -285,8 +287,9 @@ public final class GlassControlGroupComponent: Component { let size = CGSize(width: contentsWidth, height: availableSize.height) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - isInteractive = true - self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: isInteractive, transition: transition) + + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: true, transition: transition) + self.backgroundView.isUserInteractionEnabled = isInteractive return size } diff --git a/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift b/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift index 196523ecb5..51a4d2a53e 100644 --- a/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift +++ b/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift @@ -76,6 +76,7 @@ public final class GlobalControlPanelsContext { case setupBirthday case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday]) case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) + case reviewBotConnection(newBotConnectionReview: NewBotConnectionReview, botUsername: String, totalCount: Int) case premiumGrace case starsSubscriptionLowBalance(amount: StarsAmount, peers: [EnginePeer]) case setupPhoto(EnginePeer) @@ -333,6 +334,7 @@ public final class GlobalControlPanelsContext { context.engine.notices.getServerDismissedSuggestions(), twoStepData, newSessionReviews(postbox: context.account.postbox), + newBotConnectionReviews(postbox: context.account.postbox), context.engine.data.subscribe( TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId) @@ -341,12 +343,24 @@ public final class GlobalControlPanelsContext { starsSubscriptionsContextPromise.get(), accountFreezeConfiguration ) - |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext, accountFreezeConfiguration -> Signal in + |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, newBotConnectionReviews, data, birthdays, starsSubscriptionsContext, accountFreezeConfiguration -> Signal in let (accountPeer, birthday) = data if let newSessionReview = newSessionReviews.first { return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count)) } + if let newBotConnectionReview = newBotConnectionReviews.first { + return context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: newBotConnectionReview.botId) + ) + |> map { peer -> ChatListNotice? in + return .reviewBotConnection( + newBotConnectionReview: newBotConnectionReview, + botUsername: peer?.addressName ?? "", + totalCount: newBotConnectionReviews.count + ) + } + } if suggestions.contains(.setupPassword), let configuration { var notSet = false switch configuration { diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift index d01e72792f..433771e212 100644 --- a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerPackSetupController.swift @@ -448,7 +448,7 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres if isEmoji { leftNavigationButton = nil } else { - leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) } @@ -478,7 +478,7 @@ public func groupStickerPackSetupController(context: AccountContext, updatedPres enabled = true info = data.info } - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: enabled, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: enabled, action: { if info?.id == currentPackInfo?.id { dismissImpl?() } else { diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift index c3b13d0b63..559b444c02 100644 --- a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -354,15 +354,27 @@ private final class JoinSubjectScreenComponent: Component { guard let self, let component = self.component else { return } - if group.isRequest { + switch result { + case let .joined(peer): + if group.isRequest { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .inviteRequestSent(title: presentationData.strings.MemberRequests_RequestToJoinSent, text: group.isGroup ? presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } else { + if let peer { + self.navigateToPeer(peer: peer) + } + } + self.environment?.controller()?.dismiss() + case let .webView(webView): let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .inviteRequestSent(title: presentationData.strings.MemberRequests_RequestToJoinSent, text: group.isGroup ? presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup : presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel ), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } else { - if case let .joined(peer) = result, let peer { - self.navigateToPeer(peer: peer) + if let controller = self.environment?.controller() { + component.context.sharedContext.openJoinChatWebView(context: component.context, parentController: controller, updatedPresentationData: (initial: presentationData, signal: component.context.sharedContext.presentationData), webView: webView) + } + self.isJoining = false + if !self.isUpdating { + self.state?.updated(transition: .immediate) } } - self.environment?.controller()?.dismiss() }, error: { [weak self] error in guard let self, let component = self.component else { return diff --git a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift index 64f26b5236..613a72b300 100644 --- a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift @@ -10,7 +10,7 @@ import ShareController import LegacyUI import LegacyMediaPickerUI -public func presentedLegacyCamera(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { +public func presentedLegacyCamera(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)? = nil, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) @@ -78,9 +78,19 @@ public func presentedLegacyCamera(context: AccountContext, peer: EnginePeer?, ch } let paintStickersContext = LegacyPaintStickersContext(context: context) + paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: presentationData, presentInGlobalOverlay: { [weak legacyController] controller in + if let legacyController { + legacyController.presentInGlobalOverlay(controller) + } else if let mainWindow = context.sharedContext.mainWindow { + mainWindow.presentInGlobalOverlay(controller) + } else { + context.sharedContext.presentGlobalController(controller, nil) + } + }) paintStickersContext.captionPanelView = { return getCaptionPanelView() } + paintStickersContext.photoToolbarView = photoToolbarView controller.stickersContext = paintStickersContext controller.isImportant = true diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index d49f849c2b..c517dcedb9 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -804,7 +804,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { }))) let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView, position: self.currentIsCaptionAbove ? .bottom : .top)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) - self.present(contextController) + self.presentInGlobalOverlay(contextController) } private func dismissAllTooltips() { @@ -859,7 +859,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } ) self.tooltipController = tooltipController - self.present(tooltipController) + self.presentInGlobalOverlay(tooltipController) } private func presentCaptionPositionTooltip(sourceView: UIView) { @@ -900,7 +900,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } ) self.tooltipController = tooltipController - self.present(tooltipController) + self.presentInGlobalOverlay(tooltipController) let _ = ApplicationSpecificNotice.incrementCaptionAboveMediaTooltip(accountManager: self.context.sharedContext.accountManager).start() }) diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 7736448cde..f16b416515 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -519,6 +519,8 @@ public final class ListActionItemComponent: Component { case .custom: contentLeftInset += 46.0 } + } else { + contentLeftInset += component.contentInsets.left } let titleSize = self.title.update( diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift index 7a36bed129..6da3970763 100644 --- a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift +++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift @@ -473,6 +473,8 @@ public final class ListComposePollOptionComponent: Component { private var statusNode: RadialStatusNode? private var animationLayer: InlineStickerItemLayer? private var videoIconView: UIImageView? + private var webpageOverlayView: UIView? + private var webpageIconView: UIImageView? private let imageButton = HighlightTrackingButton() private var checkView: CheckView? @@ -668,6 +670,92 @@ public final class ListComposePollOptionComponent: Component { } component.attachAction?() } + + private func updateWebpageIconView(frame: CGRect, tintColor: UIColor, transition: ComponentTransition) { + var webpageIconTransition = transition + let webpageIconView: UIImageView + if let current = self.webpageIconView { + webpageIconView = current + } else { + webpageIconTransition = webpageIconTransition.withAnimation(.none) + webpageIconView = UIImageView(image: UIImage(bundleImageName: "Chat/Context Menu/Link")?.withRenderingMode(.alwaysTemplate)) + webpageIconView.isUserInteractionEnabled = false + self.webpageIconView = webpageIconView + self.addSubview(webpageIconView) + + if !transition.animation.isImmediate { + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) + alphaTransition.animateAlpha(view: webpageIconView, from: 0.0, to: 1.0) + alphaTransition.animateScale(view: webpageIconView, from: 0.01, to: 1.0) + } + } + webpageIconView.tintColor = tintColor + + let iconSize = webpageIconView.image?.size ?? CGSize(width: 30.0, height: 30.0) + let iconFrame = CGRect(origin: CGPoint(x: frame.center.x - iconSize.width * 0.5, y: frame.center.y - iconSize.height * 0.5), size: iconSize) + webpageIconTransition.setPosition(view: webpageIconView, position: iconFrame.center) + webpageIconTransition.setBounds(view: webpageIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + self.bringSubviewToFront(webpageIconView) + } + + private func removeWebpageIconView(transition: ComponentTransition) { + if let webpageIconView = self.webpageIconView { + self.webpageIconView = nil + if !transition.animation.isImmediate { + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) + alphaTransition.setAlpha(view: webpageIconView, alpha: 0.0, completion: { [weak webpageIconView] _ in + webpageIconView?.removeFromSuperview() + }) + alphaTransition.setScale(view: webpageIconView, scale: 0.001) + } else { + webpageIconView.removeFromSuperview() + } + } + } + + private func updateWebpageOverlayView(frame: CGRect, transition: ComponentTransition) { + var webpageOverlayTransition = transition + let webpageOverlayView: UIView + if let current = self.webpageOverlayView { + webpageOverlayView = current + } else { + webpageOverlayTransition = webpageOverlayTransition.withAnimation(.none) + webpageOverlayView = UIView() + webpageOverlayView.isUserInteractionEnabled = false + webpageOverlayView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3) + webpageOverlayView.layer.cornerRadius = 10.0 + webpageOverlayView.clipsToBounds = true + self.webpageOverlayView = webpageOverlayView + self.addSubview(webpageOverlayView) + + if !transition.animation.isImmediate { + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) + alphaTransition.animateAlpha(view: webpageOverlayView, from: 0.0, to: 1.0) + alphaTransition.animateScale(view: webpageOverlayView, from: 0.01, to: 1.0) + } + } + + if let webpageIconView = self.webpageIconView { + self.insertSubview(webpageOverlayView, belowSubview: webpageIconView) + } + webpageOverlayTransition.setPosition(view: webpageOverlayView, position: frame.center) + webpageOverlayTransition.setBounds(view: webpageOverlayView, bounds: CGRect(origin: CGPoint(), size: frame.size)) + } + + private func removeWebpageOverlayView(transition: ComponentTransition) { + if let webpageOverlayView = self.webpageOverlayView { + self.webpageOverlayView = nil + if !transition.animation.isImmediate { + let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2) + alphaTransition.setAlpha(view: webpageOverlayView, alpha: 0.0, completion: { [weak webpageOverlayView] _ in + webpageOverlayView?.removeFromSuperview() + }) + alphaTransition.setScale(view: webpageOverlayView, scale: 0.001) + } else { + webpageOverlayView.removeFromSuperview() + } + } + } func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true @@ -1047,13 +1135,15 @@ public final class ListComposePollOptionComponent: Component { imageNodeTransition.setBounds(view: imageNode.view, bounds: CGRect(origin: CGPoint(), size: imageNodeFrame.size)) var imageSize = imageNodeSize - var updateMedia = false - if self.appliedMedia != media { + let emptyColor = component.theme.list.mediaPlaceholderColor + let updateMedia = self.appliedMedia != media || previousComponent?.theme !== component.theme + if updateMedia { self.appliedMedia = media - updateMedia = true } var isVideo = false + var webpageIconTintColor: UIColor? + var webpageHasImageThumbnail = false if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations), let photoReference = media.concrete(TelegramMediaImage.self) { imageSize = largest.dimensions.cgSize.aspectFilled(imageNodeSize) @@ -1092,6 +1182,35 @@ public final class ListComposePollOptionComponent: Component { return context })) } + } else if let webpage = media.media as? TelegramMediaWebpage { + if case let .Loaded(content) = webpage.content, let image = content.image { + if let largest = largestImageRepresentation(image.representations) { + imageSize = largest.dimensions.cgSize.aspectFilled(imageNodeSize) + } + if updateMedia { + imageNode.setSignal(chatMessagePhoto(postbox: component.context.account.postbox, userLocation: .other, photoReference: .webPage(webPage: WebpageReference(webpage), media: image))) + if let representation = smallestImageRepresentation(image.representations) { + let _ = fetchedMediaResource(mediaBox: component.context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: .media(media: .webPage(webPage: WebpageReference(webpage), media: image), resource: representation.resource)).startStandalone() + } + } + webpageIconTintColor = .white + webpageHasImageThumbnail = true + } else { + if updateMedia { + imageNode.setSignal(.single({ arguments in + let size = arguments.imageSize + let context = DrawingContext(size: size)! + context.withFlippedContext { context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(component.theme.list.itemAccentColor.withAlphaComponent(0.1).cgColor) + context.addPath(CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil)) + context.fillPath() + } + return context + })) + } + webpageIconTintColor = component.theme.list.itemAccentColor + } } else if let map = media.media as? TelegramMediaMap { imageSize = CGSize(width: 40.0, height: 40.0) if updateMedia { @@ -1103,7 +1222,7 @@ public final class ListComposePollOptionComponent: Component { let cornerRadius: CGFloat = 10.0 let makeLayout = imageNode.asyncLayout() Queue.concurrentDefaultQueue().async { - let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: cornerRadius), imageSize: imageSize, boundingSize: imageNodeSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor)) + let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: cornerRadius), imageSize: imageSize, boundingSize: imageNodeSize, intrinsicInsets: UIEdgeInsets(), emptyColor: emptyColor)) Queue.mainQueue().async { apply() } @@ -1130,6 +1249,8 @@ public final class ListComposePollOptionComponent: Component { statusNode.transitionToState(.progress(color: .white, lineWidth: 2.0 - UIScreenPixel, value: max(0.027, min(1.0, progress)), cancelEnabled: true, animateRotation: true)) isVideo = false + self.removeWebpageIconView(transition: transition) + self.removeWebpageOverlayView(transition: transition) } else if let statusNode = self.statusNode { self.statusNode = nil if !transition.animation.isImmediate { @@ -1143,6 +1264,20 @@ public final class ListComposePollOptionComponent: Component { } } + if attachment.progress == nil { + if let webpageIconTintColor { + if webpageHasImageThumbnail { + self.updateWebpageOverlayView(frame: imageNodeFrame, transition: transition) + } else { + self.removeWebpageOverlayView(transition: transition) + } + self.updateWebpageIconView(frame: imageNodeFrame, tintColor: webpageIconTintColor, transition: transition) + } else { + self.removeWebpageIconView(transition: transition) + self.removeWebpageOverlayView(transition: transition) + } + } + if isVideo { let videoIconView: UIImageView if let current = self.videoIconView { @@ -1185,6 +1320,8 @@ public final class ListComposePollOptionComponent: Component { imageNode.view.removeFromSuperview() } self.imageButton.removeFromSuperview() + self.removeWebpageIconView(transition: transition) + self.removeWebpageOverlayView(transition: transition) if let videoIconView = self.videoIconView { self.videoIconView = nil @@ -1211,6 +1348,9 @@ public final class ListComposePollOptionComponent: Component { statusNode.view.removeFromSuperview() } } + } else { + self.removeWebpageIconView(transition: transition) + self.removeWebpageOverlayView(transition: transition) } if let inputMode = component.inputMode { diff --git a/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LiveLocationHeaderPanelComponent.swift b/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LiveLocationHeaderPanelComponent.swift index 325d2c451b..837de7f552 100644 --- a/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LiveLocationHeaderPanelComponent.swift +++ b/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LiveLocationHeaderPanelComponent.swift @@ -174,7 +174,7 @@ public final class LiveLocationHeaderPanelComponent: Component { presentLiveLocationController(context: component.context, peerId: peerId, controller: controller) } }, - close: { [weak self] in + close: { [weak self] _ in guard let self, let component = self.component, let controller = component.controller() else { return } diff --git a/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift b/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift index 6bc4d95cee..ac9479580c 100644 --- a/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift +++ b/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift @@ -25,7 +25,7 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { private var nameDisplayOrder: PresentationPersonNameOrder private let tapAction: () -> Void - private let close: () -> Void + private let close: (UIView) -> Void private let contentNode: ASDisplayNode @@ -38,7 +38,7 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { private var validLayout: (CGSize, CGFloat, CGFloat)? private var peersAndMode: ([EnginePeer], LocationBroadcastNavigationAccessoryPanelMode, Bool)? - init(accountPeerId: EnginePeer.Id, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, tapAction: @escaping () -> Void, close: @escaping () -> Void) { + init(accountPeerId: EnginePeer.Id, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, tapAction: @escaping () -> Void, close: @escaping (UIView) -> Void) { self.accountPeerId = accountPeerId self.theme = theme self.strings = strings @@ -182,15 +182,15 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { let minimizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleLayout.size.width) / 2.0), y: 22.0), size: subtitleLayout.size) if let image = self.iconNode.image { - transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: 7.0 + leftInset, y: 9.0), size: image.size)) - transition.updateFrame(node: self.wavesNode, frame: CGRect(origin: CGPoint(x: -2.0 + leftInset, y: -3.0), size: CGSize(width: 48.0, height: 48.0))) + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: 9.0 + leftInset, y: 10.0), size: image.size)) + transition.updateFrame(node: self.wavesNode, frame: CGRect(origin: CGPoint(x: leftInset, y: -3.0), size: CGSize(width: 48.0, height: 48.0))) } transition.updateFrame(node: self.titleNode, frame: minimizedTitleFrame) transition.updateFrame(node: self.subtitleNode, frame: minimizedSubtitleFrame) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width - rightInset, y: minimizedTitleFrame.minY + 10.0), size: closeButtonSize)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: size.width - 18.0 - closeButtonSize.width - rightInset, y: floorToScreenPixels((size.height - closeButtonSize.height) / 2.0)), size: closeButtonSize)) } func update(peers: [EnginePeer], mode: LocationBroadcastNavigationAccessoryPanelMode, canClose: Bool) { @@ -201,7 +201,7 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { } @objc func closePressed() { - self.close() + self.close(self.closeButton.view) } @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { 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/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 229c8257ae..f99112df82 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -44,6 +44,7 @@ swift_library( "//submodules/ChatPresentationInterfaceState", "//submodules/DeviceAccess", "//submodules/LocationUI", + "//submodules/Weather", "//submodules/TelegramUI/Components/AudioWaveformComponent", "//submodules/ReactionSelectionNode", "//submodules/TelegramUI/Components/VolumeSliderContextItem", @@ -63,7 +64,6 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/WebsiteType", "//submodules/UrlEscaping", - "//submodules/DeviceLocationManager", "//submodules/TelegramUI/Components/SaveProgressScreen", "//submodules/TelegramUI/Components/MediaAssetsContext", "//submodules/CheckNode", @@ -71,6 +71,7 @@ swift_library( "//submodules/TelegramUI/Components/AttachmentFileController", "//submodules/SaveToCameraRoll", "//submodules/TelegramUI/Components/ContextControllerImpl", + "//submodules/TelegramUI/Components/SliderComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift index 0b23bc037d..8d75439a5b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift @@ -2,11 +2,10 @@ import Foundation import UIKit import Display import ComponentFlow -import SwiftSignalKit -import LegacyComponents import MediaEditor +import SliderComponent -final class AdjustmentSliderComponent: Component { +private final class AdjustmentSliderRowComponent: Component { typealias EnvironmentType = Empty let title: String @@ -44,7 +43,7 @@ final class AdjustmentSliderComponent: Component { self.isTrackingUpdated = isTrackingUpdated } - static func ==(lhs: AdjustmentSliderComponent, rhs: AdjustmentSliderComponent) -> Bool { + static func ==(lhs: AdjustmentSliderRowComponent, rhs: AdjustmentSliderRowComponent) -> Bool { if lhs.title != rhs.title { return false } @@ -72,12 +71,12 @@ final class AdjustmentSliderComponent: Component { return true } - final class View: UIView, UITextFieldDelegate { + final class View: UIView { private let title = ComponentView() private let value = ComponentView() - private var sliderView: TGPhotoEditorSliderView? + private let slider = ComponentView() - private var component: AdjustmentSliderComponent? + private var component: AdjustmentSliderRowComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { @@ -88,22 +87,15 @@ final class AdjustmentSliderComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: AdjustmentSliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(component: AdjustmentSliderRowComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.state = state var internalIsTrackingUpdated: ((Bool) -> Void)? if let isTrackingUpdated = component.isTrackingUpdated { internalIsTrackingUpdated = { [weak self] isTracking in + isTrackingUpdated(isTracking) if let self { - if isTracking { - self.sliderView?.bordered = true - } else { - Queue.mainQueue().after(0.1) { - self.sliderView?.bordered = false - } - } - isTrackingUpdated(isTracking) let transition: ComponentTransition if isTracking { transition = .immediate @@ -119,52 +111,27 @@ final class AdjustmentSliderComponent: Component { } } } - - let sliderView: TGPhotoEditorSliderView - if let current = self.sliderView { - sliderView = current - sliderView.value = CGFloat(component.value) - } else { - sliderView = TGPhotoEditorSliderView() - sliderView.backgroundColor = .clear - sliderView.startColor = UIColor(rgb: 0xffffff) - sliderView.enablePanHandling = true - sliderView.trackCornerRadius = 1.0 - sliderView.lineSize = 2.0 - sliderView.minimumValue = CGFloat(component.minValue) - sliderView.maximumValue = CGFloat(component.maxValue) - sliderView.startValue = CGFloat(component.startValue) - sliderView.value = CGFloat(component.value) - sliderView.disablesInteractiveTransitionGestureRecognizer = true - sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) - sliderView.layer.allowsGroupOpacity = true - self.sliderView = sliderView - self.addSubview(sliderView) - } - sliderView.interactionBegan = { - internalIsTrackingUpdated?(true) - } - sliderView.interactionEnded = { - internalIsTrackingUpdated?(false) - } - if component.isEnabled { - sliderView.alpha = 1.3 - sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff) - sliderView.isUserInteractionEnabled = true + var hasValue = false + let valueText: String + if component.displayValue { + if component.value > 0.005 { + valueText = String(format: "+%.2f", component.value) + hasValue = true + } else if component.value < -0.005 { + valueText = String(format: "%.2f", component.value) + hasValue = true + } else { + valueText = "" + } } else { - sliderView.trackColor = UIColor(rgb: 0xffffff) - sliderView.alpha = 0.3 - sliderView.isUserInteractionEnabled = false + valueText = "" } - transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0))) - sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) - let titleSize = self.title.update( transition: .immediate, component: AnyComponent( - Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080)) + Text(text: component.title, font: Font.bold(13.0), color: hasValue ? UIColor(rgb: 0xffffff) : UIColor(rgb: 0x808080)) ), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) @@ -173,26 +140,13 @@ final class AdjustmentSliderComponent: Component { if titleView.superview == nil { self.addSubview(titleView) } - transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize)) - } - - let valueText: String - if component.displayValue { - if component.value > 0.005 { - valueText = String(format: "+%.2f", component.value) - } else if component.value < -0.005 { - valueText = String(format: "%.2f", component.value) - } else { - valueText = "" - } - } else { - valueText = "" + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 30.0, y: 0.0), size: titleSize)) } let valueSize = self.value.update( transition: .immediate, component: AnyComponent( - Text(text: valueText, font: Font.with(size: 14.0, traits: .monospacedNumbers), color: UIColor(rgb: 0xffd300)) + Text(text: valueText, font: Font.with(size: 13.0, weight: .medium, traits: .monospacedNumbers), color: UIColor(rgb: 0xffd300)) ), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0) @@ -201,17 +155,44 @@ final class AdjustmentSliderComponent: Component { if valueView.superview == nil { self.addSubview(valueView) } - transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 21.0 - valueSize.width, y: 0.0), size: valueSize)) + transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 30.0 - valueSize.width, y: 0.0), size: valueSize)) } - return CGSize(width: availableSize.width, height: 52.0) - } - - @objc private func sliderValueChanged() { - guard let component = self.component, let sliderView = self.sliderView else { - return + let sliderSize = self.slider.update( + transition: transition, + component: AnyComponent( + SliderComponent( + content: .continuous(SliderComponent.Continuous( + value: CGFloat(component.value), + range: CGFloat(component.minValue) ... CGFloat(component.maxValue), + startValue: CGFloat(component.startValue), + valueUpdated: { [weak self] value in + guard let self, let component = self.component else { + return + } + component.valueUpdated(Float(value)) + } + )), + useNative: true, + trackBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), + trackForegroundColor: component.isEnabled ? (component.trackColor ?? UIColor(rgb: 0xffffff)) : UIColor(rgb: 0xffffff), + isEnabled: component.isEnabled, + trackHeight: 6.0, + displaysBorderOnTracking: component.isTrackingUpdated != nil, + isTrackingUpdated: internalIsTrackingUpdated + ) + ), + environment: {}, + containerSize: CGSize(width: max(0.0, availableSize.width - 28.0 * 2.0), height: 44.0) + ) + if let sliderView = self.slider.view { + if sliderView.superview == nil { + self.addSubview(sliderView) + } + transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 28.0, y: 7.0), size: sliderSize)) } - component.valueUpdated(Float(sliderView.value)) + + return CGSize(width: availableSize.width, height: 52.0) } } @@ -325,14 +306,14 @@ final class AdjustmentsComponent: Component { let size = componentView.update( transition: transition, component: AnyComponent( - AdjustmentSliderComponent( + AdjustmentSliderRowComponent( title: tool.title, value: value, minValue: tool.minValue, maxValue: tool.maxValue, startValue: tool.startValue, isEnabled: true, - trackColor: nil, + trackColor: UIColor(rgb: 0xffd300), displayValue: true, valueUpdated: { value in var updatedValue = value @@ -363,7 +344,7 @@ final class AdjustmentsComponent: Component { } transition.setFrame(view: view, frame: CGRect(origin: origin, size: size)) } - origin = origin.offsetBy(dx: 0.0, dy: size.height) + origin = origin.offsetBy(dx: 0.0, dy: size.height + 8.0) } let size = CGSize(width: availableSize.width, height: 180.0) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift index 3cf59203b7..47019ceb17 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/BlurComponent.swift @@ -2,9 +2,9 @@ import Foundation import UIKit import Display import ComponentFlow -import LegacyComponents import MediaEditor import TelegramPresentationData +import SliderComponent private final class BlurModeComponent: Component { typealias EnvironmentType = Empty @@ -73,7 +73,7 @@ private final class BlurModeComponent: Component { component: AnyComponent( Text( text: component.title, - font: Font.regular(14.0), + font: Font.bold(13.0), color: component.isSelected ? UIColor(rgb: 0xffd300) : UIColor(rgb: 0x808080) ) ), @@ -198,7 +198,7 @@ final class BlurComponent: Component { component: AnyComponent( Text( text: component.strings.Story_Editor_Blur_Title, - font: Font.regular(14.0), + font: Font.bold(13.0), color: UIColor(rgb: 0x808080) ) ), @@ -331,27 +331,30 @@ final class BlurComponent: Component { let sliderSize = self.slider.update( transition: transition, component: AnyComponent( - AdjustmentSliderComponent( - title: "", - value: state.value.intensity, - minValue: 0.0, - maxValue: 1.0, - startValue: 0.0, - isEnabled: state.value.mode != .off, - trackColor: nil, - displayValue: false, - valueUpdated: { [weak state] value in - if let state { - valueUpdated(state.value.withUpdatedIntensity(value)) + SliderComponent( + content: .continuous(SliderComponent.Continuous( + value: CGFloat(state.value.intensity), + range: 0.0 ... 1.0, + startValue: 0.0, + valueUpdated: { [weak state] value in + if let state { + valueUpdated(state.value.withUpdatedIntensity(Float(value))) + } } - }, + )), + useNative: true, + trackBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), + trackForegroundColor: state.value.mode != .off ? UIColor(rgb: 0xffd300) : UIColor(rgb: 0xffffff), + isEnabled: state.value.mode != .off, + trackHeight: 6.0, + displaysBorderOnTracking: true, isTrackingUpdated: { isTracking in isTrackingUpdated(isTracking) } ) ), environment: {}, - containerSize: availableSize + containerSize: CGSize(width: max(0.0, availableSize.width - 28.0 * 2.0), height: 44.0) ) var buttons = [self.offButton, self.radialButton, self.linearButton] @@ -375,7 +378,8 @@ final class BlurComponent: Component { } let verticalSpacing: CGFloat = -5.0 - let sliderFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + offButtonSize.height + verticalSpacing), size: sliderSize) + let sliderRowHeight: CGFloat = 52.0 + let sliderFrame = CGRect(origin: CGPoint(x: 28.0, y: topInset + offButtonSize.height + verticalSpacing + 7.0), size: sliderSize) if let view = self.slider.view { if view.superview == nil { self.addSubview(view) @@ -383,7 +387,7 @@ final class BlurComponent: Component { transition.setFrame(view: view, frame: sliderFrame) } - return CGSize(width: availableSize.width, height: topInset + offButtonSize.height + verticalSpacing + sliderSize.height + 6.0) + return CGSize(width: availableSize.width, height: topInset + offButtonSize.height + verticalSpacing + sliderRowHeight + 6.0) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift index 924459dc45..7311d63ff8 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CurvesComponent.swift @@ -153,7 +153,7 @@ final class CurvesComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Curves_All, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .all ? .white : UIColor(rgb: 0x808080) ) ), @@ -181,7 +181,7 @@ final class CurvesComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Curves_Red, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .red ? .white : UIColor(rgb: 0x808080) ) ), @@ -209,7 +209,7 @@ final class CurvesComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Curves_Green, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .green ? .white : UIColor(rgb: 0x808080) ) ), @@ -237,7 +237,7 @@ final class CurvesComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Curves_Blue, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .blue ? .white : UIColor(rgb: 0x808080) ) ), @@ -536,7 +536,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.blacks), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), @@ -563,7 +563,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.shadows), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), @@ -590,7 +590,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.midtones), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), @@ -617,7 +617,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.highlights), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), @@ -644,7 +644,7 @@ final class CurvesScreenComponent: Component { text: .plain( NSAttributedString( string: String(format: "%.2f", value.whites), - font: Font.regular(14.0), + font: Font.medium(13.0), textColor: UIColor(rgb: 0xffffff) ) ), diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift index 1b958dfa1b..f58ce5a41f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift @@ -230,6 +230,7 @@ private final class MediaCoverScreenComponent: Component { component: AnyComponent( ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) @@ -261,7 +262,7 @@ private final class MediaCoverScreenComponent: Component { ) ), environment: {}, - containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) ) let doneButtonFrame = CGRect( origin: CGPoint(x: floor((availableSize.width - doneButtonSize.width) / 2.0), y: min(buttonsContainerFrame.minY, availableSize.height - doneButtonSize.height - buttonSideInset)), diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 5a77bf9af7..9e8d198831 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -52,6 +52,9 @@ import SaveProgressScreen import TelegramNotices import AttachmentFileController import SaveToCameraRoll +import GlassBarButtonComponent +import GlassBackgroundComponent +import Weather private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -198,7 +201,7 @@ final class MediaEditorScreenComponent: Component { case .sticker: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddSticker"), color: .white)! case .tools: - image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)! + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Adjustments"), color: .white)! case .rotate: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Rotate"), color: .white)! case .flip: @@ -300,6 +303,8 @@ final class MediaEditorScreenComponent: Component { public final class View: UIView { private let cancelButton = ComponentView() private let doneButton = ComponentView() + + private let buttonsBackgroundView = GlassBackgroundView() private let drawButton = ComponentView() private let textButton = ComponentView() private let stickerButton = ComponentView() @@ -675,9 +680,6 @@ final class MediaEditorScreenComponent: Component { } func animateOutToTool(inPlace: Bool, transition: ComponentTransition) { - if let view = self.cancelButton.view { - view.alpha = 0.0 - } let buttons = [ self.drawButton, self.textButton, @@ -686,12 +688,12 @@ final class MediaEditorScreenComponent: Component { ] for button in buttons { if let view = button.view { - if !inPlace { - view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } + if let view = self.cancelButton.view { + transition.setScale(view: view, scale: 0.1) + } if let view = self.doneButton.view { transition.setScale(view: view, scale: 0.1) } @@ -704,12 +706,6 @@ final class MediaEditorScreenComponent: Component { } func animateInFromTool(inPlace: Bool, transition: ComponentTransition) { - if let view = self.cancelButton.view { - view.alpha = 1.0 - } - if let buttonView = self.cancelButton.view as? Button.View, let view = buttonView.content as? LottieAnimationComponent.View { - view.playOnce() - } let buttons = [ self.drawButton, self.textButton, @@ -718,12 +714,12 @@ final class MediaEditorScreenComponent: Component { ] for button in buttons { if let view = button.view { - if !inPlace { - view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } } + if let view = self.cancelButton.view { + transition.setScale(view: view, scale: 1.0) + } if let view = self.doneButton.view { transition.setScale(view: view, scale: 1.0) } @@ -821,10 +817,17 @@ final class MediaEditorScreenComponent: Component { buttonSideInset = 30.0 } else { previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) - buttonSideInset = 10.0 + if availableSize.height < previewSize.height + 30.0 { topInset = 0.0 - controlsBottomInset = -50.0 + controlsBottomInset = -62.0 + buttonSideInset = 9.0 + } else { + if availableSize.width > 320.0 { + buttonSideInset = 16.0 + } else { + buttonSideInset = 9.0 + } } } var previewFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: topInset), size: previewSize) @@ -835,30 +838,36 @@ final class MediaEditorScreenComponent: Component { let bottomButtonsAlpha: CGFloat = isRecordingAdditionalVideo ? 0.3 : 1.0 let buttonsAreHidden = component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities + if self.buttonsBackgroundView.superview == nil { + self.addSubview(self.buttonsBackgroundView) + } + let cancelButtonSize = self.cancelButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "media_backToCancel", - mode: .still(position: .end), - range: (0.5, 1.0) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 33.0, height: 33.0) - ) - ), - action: { [weak controller] in - guard let controller else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + isVisible: !buttonsAreHidden && bottomButtonsAlpha > 0.0, + component: AnyComponentWithIdentity( + id: "close", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Back", tintColor: .white) + ) + ), + action: { [weak controller] _ in + guard let controller else { + return + } + guard !controller.node.recording.isActive else { + return + } + controller.maybePresentDiscardAlert() } - guard !controller.node.recording.isActive else { - return - } - controller.maybePresentDiscardAlert() - } - )), + ) + ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) @@ -872,35 +881,83 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center) transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size)) - transition.setAlpha(view: cancelButtonView, alpha: buttonsAreHidden ? 0.0 : bottomButtonsAlpha) } - var doneButtonTitle: String? - var doneButtonIcon: UIImage? + let doneButtonFont = Font.with(size: 16.0, design: .round, weight: .semibold) + var doneButtonItems: [AnyComponentWithIdentity] = [] + var doneButtonExplicitSize: CGSize? switch controller.mode { case .storyEditor: - doneButtonTitle = isEditingStory ? environment.strings.Story_Editor_Done.uppercased() : environment.strings.Story_Editor_Next.uppercased() - doneButtonIcon = UIImage(bundleImageName: "Media Editor/Next")! + if availableSize.width > 320.0 { + doneButtonItems = [ + AnyComponentWithIdentity( + id: "label", + component: AnyComponent( + Text(text: isEditingStory ? environment.strings.Story_Editor_Done.uppercased() : environment.strings.Story_Editor_Next.uppercased(), font: doneButtonFont, color: .white) + ) + ), + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Media Editor/Next", tintColor: .white) + ) + ) + ] + } else { + doneButtonExplicitSize = CGSize(width: 44.0, height: 44.0) + doneButtonItems = [ + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Back", tintColor: .white, flipHorizontally: true) + ) + ) + ] + } case .stickerEditor, .avatarEditor, .coverEditor: - doneButtonTitle = nil - doneButtonIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Apply"), color: .white)! + doneButtonExplicitSize = CGSize(width: 44.0, height: 44.0) + doneButtonItems = [ + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Done", tintColor: .white) + ) + ) + ] case .botPreview: - doneButtonTitle = environment.strings.Story_Editor_Add.uppercased() - doneButtonIcon = nil + doneButtonItems = [ + AnyComponentWithIdentity( + id: "label", + component: AnyComponent( + Text(text: environment.strings.Story_Editor_Add.uppercased(), font: doneButtonFont, color: .white) + ) + ) + ] } let doneButtonSize = self.doneButton.update( transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(DoneButtonContentComponent( + component: AnyComponent( + GlassBarButtonComponent( + size: doneButtonExplicitSize, backgroundColor: UIColor(rgb: 0x0088ff), - icon: doneButtonIcon, - title: doneButtonTitle)), - effectAlignment: .center, - action: { [weak controller] in - controller?.node.requestCompletion() - } - )), + isDark: true, + state: .tintedGlass, + isVisible: !buttonsAreHidden && bottomButtonsAlpha > 0.0, + component: AnyComponentWithIdentity( + id: "done", + component: AnyComponent( + HStack( + doneButtonItems, + spacing: 5.0 + ) + ) + ), + action: { [weak controller] _ in + controller?.node.requestCompletion() + } + ) + ), environment: {}, containerSize: CGSize(width: availableSize.width, height: 44.0) ) @@ -914,19 +971,21 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: doneButtonView, position: doneButtonFrame.center) transition.setBounds(view: doneButtonView, bounds: CGRect(origin: .zero, size: doneButtonFrame.size)) - transition.setAlpha(view: doneButtonView, alpha: buttonsAreHidden ? 0.0 : bottomButtonsAlpha) } - - let buttonsAvailableWidth: CGFloat - let buttonsLeftOffset: CGFloat - if isTablet { - buttonsAvailableWidth = previewSize.width + 180.0 - buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0) - } else { - buttonsAvailableWidth = floor(availableSize.width - cancelButtonSize.width * 0.66 - (doneButtonSize.width - cancelButtonSize.width * 0.33) - buttonSideInset * 2.0) - buttonsLeftOffset = floorToScreenPixels(buttonSideInset + cancelButtonSize.width * 0.66) + + let buttonSize = CGSize(width: 44.0, height: 44.0) + let buttonSpacing: CGFloat = 10.0 + var buttonCount = 4 + if let subject = controller.node.subject, case .empty = subject { + buttonCount = 3 } + let buttonsTotalWidth: CGFloat = buttonSize.width * CGFloat(buttonCount) + buttonSpacing * CGFloat(buttonCount - 1) + let buttonsBackgroundFrame = CGRect(x: cancelButtonFrame.maxX + floorToScreenPixels(((doneButtonFrame.minX - cancelButtonFrame.maxX) - buttonsTotalWidth) / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset, width: buttonsTotalWidth, height: buttonSize.height) + transition.setFrame(view: self.buttonsBackgroundView, frame: buttonsBackgroundFrame) + self.buttonsBackgroundView.update(size: buttonsBackgroundFrame.size, cornerRadius: buttonsBackgroundFrame.size.height * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, isVisible: !buttonsAreHidden && bottomButtonsAlpha > 0.0, transition: transition) + + var buttonOriginX: CGFloat = 0.0 let drawButtonSize = self.drawButton.update( transition: transition, component: AnyComponent(ContextReferenceButtonComponent( @@ -935,7 +994,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: drawButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller] _, _ in guard let controller else { return @@ -947,13 +1006,14 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) var drawButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 - drawButtonSize.width / 2.0 - 3.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 1.0), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: drawButtonSize - ) - + ) + buttonOriginX += drawButtonSize.width + buttonSpacing + let textButtonSize = self.textButton.update( transition: transition, component: AnyComponent(ContextReferenceButtonComponent( @@ -962,7 +1022,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: textButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller] _, _ in guard let controller else { return @@ -974,12 +1034,13 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) var textButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 2.0 - textButtonSize.width / 2.0 - 1.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: textButtonSize ) + buttonOriginX += textButtonSize.width + buttonSpacing let stickerButtonSize = self.stickerButton.update( transition: transition, @@ -989,7 +1050,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: stickerButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller] view, gesture in guard let controller else { return @@ -1005,12 +1066,13 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) var stickerButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 3.0 - stickerButtonSize.width / 2.0 + 1.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: stickerButtonSize ) + buttonOriginX += drawButtonSize.width + buttonSpacing let rotateButtonSize = self.rotateButton.update( transition: transition, @@ -1020,7 +1082,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: textButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller, weak mediaEditor] _, _ in guard let controller, let mediaEditor else { return @@ -1037,10 +1099,10 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let rotateButtonFrame = CGRect( - origin: CGPoint(x: drawButtonFrame.origin.x, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), + origin: CGPoint(x: drawButtonFrame.origin.x, y: 0.0), size: rotateButtonSize ) @@ -1052,7 +1114,7 @@ final class MediaEditorScreenComponent: Component { size: CGSize(width: 30.0, height: 30.0) )), tag: textButtonTag, - minSize: CGSize(width: 30.0, height: 30.0), + minSize: buttonSize, action: { [weak controller, weak mediaEditor] _, _ in guard let controller, let mediaEditor else { return @@ -1069,10 +1131,10 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let flipButtonFrame = CGRect( - origin: CGPoint(x: textButtonFrame.origin.x, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0), + origin: CGPoint(x: textButtonFrame.origin.x, y: 0.0), size: flipButtonSize ) @@ -1089,7 +1151,7 @@ final class MediaEditorScreenComponent: Component { if let rotateButtonView = self.rotateButton.view { if rotateButtonView.superview == nil { - self.addSubview(rotateButtonView) + self.buttonsBackgroundView.contentView.addSubview(rotateButtonView) } transition.setPosition(view: rotateButtonView, position: rotateButtonFrame.center) transition.setBounds(view: rotateButtonView, bounds: CGRect(origin: .zero, size: rotateButtonFrame.size)) @@ -1100,7 +1162,7 @@ final class MediaEditorScreenComponent: Component { if let flipButtonView = self.flipButton.view { if flipButtonView.superview == nil { - self.addSubview(flipButtonView) + self.buttonsBackgroundView.contentView.addSubview(flipButtonView) } transition.setPosition(view: flipButtonView, position: flipButtonFrame.center) transition.setBounds(view: flipButtonView, bounds: CGRect(origin: .zero, size: flipButtonFrame.size)) @@ -1117,7 +1179,7 @@ final class MediaEditorScreenComponent: Component { if let drawButtonView = self.drawButton.view { if drawButtonView.superview == nil { - self.addSubview(drawButtonView) + self.buttonsBackgroundView.contentView.addSubview(drawButtonView) } transition.setPosition(view: drawButtonView, position: drawButtonFrame.center) transition.setBounds(view: drawButtonView, bounds: CGRect(origin: .zero, size: drawButtonFrame.size)) @@ -1128,7 +1190,7 @@ final class MediaEditorScreenComponent: Component { if !isAvatarEditor && !isCoverEditor, let textButtonView = self.textButton.view { if textButtonView.superview == nil { - self.addSubview(textButtonView) + self.buttonsBackgroundView.contentView.addSubview(textButtonView) } transition.setPosition(view: textButtonView, position: textButtonFrame.center) transition.setBounds(view: textButtonView, bounds: CGRect(origin: .zero, size: textButtonFrame.size)) @@ -1139,7 +1201,7 @@ final class MediaEditorScreenComponent: Component { if !isAvatarEditor && !isCoverEditor, let stickerButtonView = self.stickerButton.view { if stickerButtonView.superview == nil { - self.addSubview(stickerButtonView) + self.buttonsBackgroundView.contentView.addSubview(stickerButtonView) } transition.setPosition(view: stickerButtonView, position: stickerButtonFrame.center) transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: .zero, size: stickerButtonFrame.size)) @@ -1155,12 +1217,14 @@ final class MediaEditorScreenComponent: Component { } else { let toolsButtonSize = self.toolsButton.update( transition: transition, - component: AnyComponent(Button( + component: AnyComponent(ContextReferenceButtonComponent( content: AnyComponent(Image( image: state.image(.tools), size: CGSize(width: 30.0, height: 30.0) )), - action: { [weak controller] in + tag: textButtonTag, + minSize: buttonSize, + action: { [weak controller] _, _ in guard let controller else { return } @@ -1171,15 +1235,15 @@ final class MediaEditorScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let toolsButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 4.0 - toolsButtonSize.width / 2.0 + 3.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 1.0), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: toolsButtonSize ) if let toolsButtonView = self.toolsButton.view { if toolsButtonView.superview == nil { - self.addSubview(toolsButtonView) + self.buttonsBackgroundView.contentView.addSubview(toolsButtonView) } transition.setPosition(view: toolsButtonView, position: toolsButtonFrame.center) transition.setBounds(view: toolsButtonView, bounds: CGRect(origin: .zero, size: toolsButtonFrame.size)) @@ -1643,31 +1707,37 @@ final class MediaEditorScreenComponent: Component { let saveButtonSize = self.saveButton.update( transition: transition, - component: AnyComponent(CameraButton( - content: saveContentComponent, - action: { [weak self, weak controller] in - guard let self, let controller else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + isVisible: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && topButtonsAlpha > 0.0, + component: saveContentComponent, + action: { [weak self, weak controller] _ in + guard let self, let controller else { + return + } + guard !controller.node.recording.isActive else { + return + } + if let view = self.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { + view.playOnce() + } + controller.requestSave() } - guard !controller.node.recording.isActive else { - return - } - if let view = self.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { - view.playOnce() - } - controller.requestSave() - } - )), + ) + ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) let saveButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + origin: CGPoint(x: availableSize.width - 16.0 - saveButtonSize.width, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), size: saveButtonSize ) if let saveButtonView = self.saveButton.view { if saveButtonView.superview == nil { - setupButtonShadow(saveButtonView) self.addSubview(saveButtonView) } @@ -1677,7 +1747,7 @@ final class MediaEditorScreenComponent: Component { buttonTransition.setPosition(view: saveButtonView, position: saveButtonFrame.center) buttonTransition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) transition.setScale(view: saveButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? saveButtonAlpha : 0.0) + transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && saveButtonAlpha > 0.0 ? saveButtonAlpha : 1.0) } var topButtonOffsetX: CGFloat = 0.0 @@ -9230,3 +9300,31 @@ private struct MediaEditorConfiguration { } } } + +private func getWeather(context: AccountContext, load: Bool) -> Signal { + return Weather.getWeatherData(context: context, load: load) + |> map { state -> StickerPickerScreen.Weather in + switch state { + case .none: + return .none + case .notDetermined: + return .notDetermined + case .notAllowed: + return .notAllowed + case .notPreloaded: + return .notPreloaded + case .fetching: + return .fetching + case let .loaded(weather): + if let match = context.animatedEmojiStickersValue[weather.emoji]?.first { + return .loaded(StickerPickerScreen.Weather.LoadedWeather( + emoji: weather.emoji, + emojiFile: match.file._parse(), + temperature: weather.temperature + )) + } else { + return .none + } + } + } +} 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/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index ec53f4d4eb..acfb68e235 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -13,8 +13,10 @@ import MultilineTextComponent import DrawingUI import MediaEditor import Photos -import LottieAnimationComponent import MessageInputPanelComponent +import BundleIconComponent +import GlassBarButtonComponent +import GlassBackgroundComponent private enum MediaToolsSection: Equatable { case adjustments @@ -54,7 +56,6 @@ private final class ToolIconComponent: Component { } final class View: UIView { - private let selection = SimpleShapeLayer() private let icon = ComponentView() private var component: ToolIconComponent? @@ -62,10 +63,6 @@ private final class ToolIconComponent: Component { override init(frame: CGRect) { super.init(frame: frame) - - self.selection.path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), cornerRadius: 10.0).cgPath - self.selection.fillColor = UIColor(rgb: 0xd1d1d1).cgColor - self.layer.addSublayer(self.selection) } required init?(coder: NSCoder) { @@ -105,8 +102,6 @@ private final class ToolIconComponent: Component { transition.setFrame(view: view, frame: iconFrame) } - self.selection.isHidden = !component.isSelected - return size } } @@ -163,7 +158,6 @@ private final class MediaToolsScreenComponent: Component { case tint case blur case curves - case done } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -173,15 +167,13 @@ private final class MediaToolsScreenComponent: Component { var image: UIImage switch key { case .adjustments: - image = UIImage(bundleImageName: "Media Editor/Tools")! + image = UIImage(bundleImageName: "Media Editor/Adjustments")! case .tint: image = UIImage(bundleImageName: "Media Editor/Tint")! case .blur: image = UIImage(bundleImageName: "Media Editor/Blur")! case .curves: image = UIImage(bundleImageName: "Media Editor/Curves")! - case .done: - image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)! } cachedImages[key] = image return image @@ -220,7 +212,8 @@ private final class MediaToolsScreenComponent: Component { public final class View: UIView { private let buttonsContainerView = UIView() - private let buttonsBackgroundView = UIView() + private let buttonsBackgroundView = GlassBackgroundView() + private let selectedToolBackgroundLayer = SimpleShapeLayer() private let cancelButton = ComponentView() private let adjustmentsButton = ComponentView() private let tintButton = ComponentView() @@ -248,7 +241,7 @@ private final class MediaToolsScreenComponent: Component { self.optionsBackgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.9) super.init(frame: frame) - + self.backgroundColor = .clear self.addSubview(self.previewContainerView) @@ -256,6 +249,10 @@ private final class MediaToolsScreenComponent: Component { self.previewContainerView.addSubview(self.optionsContainerView) self.optionsContainerView.addSubview(self.optionsBackgroundView) self.buttonsContainerView.addSubview(self.buttonsBackgroundView) + + self.selectedToolBackgroundLayer.path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), cornerRadius: 16.5).cgPath + self.selectedToolBackgroundLayer.fillColor = UIColor(rgb: 0xd1d1d1).cgColor + self.buttonsBackgroundView.contentView.layer.insertSublayer(self.selectedToolBackgroundLayer, at: 0) } required init?(coder: NSCoder) { @@ -269,20 +266,17 @@ private final class MediaToolsScreenComponent: Component { self.blurButton, self.curvesButton ] - - var delay: Double = 0.0 for button in buttons { if let view = button.view { - view.alpha = 0.0 - Queue.mainQueue().after(delay, { - view.alpha = 1.0 - view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.0) - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) - }) - delay += 0.03 + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.0) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) } } + + if let view = self.cancelButton.view { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } if let view = self.doneButton.view { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -297,9 +291,7 @@ private final class MediaToolsScreenComponent: Component { private var animatingOut = false func animateOutToEditor(completion: @escaping () -> Void) { self.animatingOut = true - - self.cancelButton.view?.isHidden = true - + let buttons = [ self.adjustmentsButton, self.tintButton, @@ -309,17 +301,21 @@ private final class MediaToolsScreenComponent: Component { for button in buttons { if let view = button.view { - view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true, completion: { _ in + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in completion() }) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } + if let view = self.cancelButton.view { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) + } + if let view = self.doneButton.view { view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) } if let view = self.toolScreen?.view { @@ -364,12 +360,17 @@ private final class MediaToolsScreenComponent: Component { buttonSideInset = 30.0 } else { previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) - buttonSideInset = 10.0 + if availableSize.height < previewSize.height + 30.0 { topInset = 0.0 controlsBottomInset = -75.0 + buttonSideInset = 9.0 } else { - self.buttonsBackgroundView.backgroundColor = .clear + if availableSize.width > 320.0 { + buttonSideInset = 16.0 + } else { + buttonSideInset = 9.0 + } } } @@ -378,25 +379,26 @@ private final class MediaToolsScreenComponent: Component { let cancelButtonSize = self.cancelButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "media_backToCancel", - mode: .animating(loop: false), - range: self.animatingOut ? (0.5, 1.0) : (0.0, 0.5) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 33.0, height: 33.0) - ) - ), - action: { - guard let controller = environment.controller() as? MediaToolsScreen else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: true, + state: .glass, + component: AnyComponentWithIdentity( + id: "close", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Back", tintColor: .white) + ) + ), + action: { _ in + guard let controller = environment.controller() as? MediaToolsScreen else { + return + } + controller.requestDismiss(reset: true, animated: true) } - controller.requestDismiss(reset: true, animated: true) - } - )), + ) + ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) @@ -413,18 +415,26 @@ private final class MediaToolsScreenComponent: Component { let doneButtonSize = self.doneButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Image( - image: state.image(.done), - size: CGSize(width: 33.0, height: 33.0) - )), - action: { - guard let controller = environment.controller() as? MediaToolsScreen else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: UIColor(rgb: 0x0088ff), + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity( + id: "done", + component: AnyComponent( + BundleIconComponent(name: "Navigation/Done", tintColor: .white) + ) + ), + action: { _ in + guard let controller = environment.controller() as? MediaToolsScreen else { + return + } + controller.requestDismiss(reset: false, animated: true) } - controller.requestDismiss(reset: false, animated: true) - } - )), + ) + ), environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) @@ -439,15 +449,19 @@ private final class MediaToolsScreenComponent: Component { transition.setFrame(view: doneButtonView, frame: doneButtonFrame) } - let buttonsAvailableWidth: CGFloat - let buttonsLeftOffset: CGFloat - if isTablet { - buttonsAvailableWidth = previewSize.width + 260.0 - buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0) - } else { - buttonsAvailableWidth = availableSize.width - buttonsLeftOffset = 0.0 - } + let buttonSize = CGSize(width: 44.0, height: 44.0) + let buttonSpacing: CGFloat = 10.0 + let buttonsTotalWidth = buttonSize.width * 4.0 + buttonSpacing * 3.0 + let buttonsBackgroundFrame = CGRect( + x: cancelButtonFrame.maxX + floorToScreenPixels((doneButtonFrame.minX - cancelButtonFrame.maxX - buttonsTotalWidth) / 2.0), + y: buttonBottomInset, + width: buttonsTotalWidth, + height: buttonSize.height + ) + transition.setFrame(view: self.buttonsBackgroundView, frame: buttonsBackgroundFrame) + self.buttonsBackgroundView.update(size: buttonsBackgroundFrame.size, cornerRadius: buttonsBackgroundFrame.height * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) + + var buttonOriginX: CGFloat = 0.0 let adjustmentsButtonSize = self.adjustmentsButton.update( transition: transition, @@ -460,17 +474,18 @@ private final class MediaToolsScreenComponent: Component { action: { sectionUpdated(.adjustments) } - )), + ).minSize(buttonSize)), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let adjustmentsButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 - 3.0 - adjustmentsButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: adjustmentsButtonSize ) + buttonOriginX += adjustmentsButtonSize.width + buttonSpacing if let adjustmentsButtonView = self.adjustmentsButton.view { if adjustmentsButtonView.superview == nil { - self.buttonsContainerView.addSubview(adjustmentsButtonView) + self.buttonsBackgroundView.contentView.addSubview(adjustmentsButtonView) } transition.setFrame(view: adjustmentsButtonView, frame: adjustmentsButtonFrame) } @@ -486,17 +501,18 @@ private final class MediaToolsScreenComponent: Component { action: { sectionUpdated(.tint) } - )), + ).minSize(buttonSize)), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let tintButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 2.5 + 5.0 - tintButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: tintButtonSize ) + buttonOriginX += tintButtonSize.width + buttonSpacing if let tintButtonView = self.tintButton.view { if tintButtonView.superview == nil { - self.buttonsContainerView.addSubview(tintButtonView) + self.buttonsBackgroundView.contentView.addSubview(tintButtonView) } transition.setFrame(view: tintButtonView, frame: tintButtonFrame) } @@ -512,17 +528,18 @@ private final class MediaToolsScreenComponent: Component { action: { sectionUpdated(.blur) } - )), + ).minSize(buttonSize)), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let blurButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width - buttonsLeftOffset - buttonsAvailableWidth / 2.5 - 5.0 - blurButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: blurButtonSize ) + buttonOriginX += blurButtonSize.width + buttonSpacing if let blurButtonView = self.blurButton.view { if blurButtonView.superview == nil { - self.buttonsContainerView.addSubview(blurButtonView) + self.buttonsBackgroundView.contentView.addSubview(blurButtonView) } transition.setFrame(view: blurButtonView, frame: blurButtonFrame) } @@ -538,21 +555,42 @@ private final class MediaToolsScreenComponent: Component { action: { sectionUpdated(.curves) } - )), + ).minSize(buttonSize)), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: buttonSize ) let curvesButtonFrame = CGRect( - origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 * 3.0 + 3.0 - curvesButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonOriginX, y: 0.0), size: curvesButtonSize ) if let curvesButtonView = self.curvesButton.view { if curvesButtonView.superview == nil { - self.buttonsContainerView.addSubview(curvesButtonView) + self.buttonsBackgroundView.contentView.addSubview(curvesButtonView) } transition.setFrame(view: curvesButtonView, frame: curvesButtonFrame) } + let selectedButtonFrame: CGRect + switch component.section { + case .adjustments: + selectedButtonFrame = adjustmentsButtonFrame + case .tint: + selectedButtonFrame = tintButtonFrame + case .blur: + selectedButtonFrame = blurButtonFrame + case .curves: + selectedButtonFrame = curvesButtonFrame + } + let selectedToolBackgroundSize = CGSize(width: 33.0, height: 33.0) + let selectedToolBackgroundFrame = CGRect( + origin: CGPoint( + x: floorToScreenPixels(selectedButtonFrame.midX - selectedToolBackgroundSize.width / 2.0), + y: floorToScreenPixels(selectedButtonFrame.midY - selectedToolBackgroundSize.height / 2.0) + ), + size: selectedToolBackgroundSize + ) + transition.setFrame(layer: self.selectedToolBackgroundLayer, frame: selectedToolBackgroundFrame) + var sectionChanged = false if previousSection != component.section { sectionChanged = true @@ -938,7 +976,6 @@ private final class MediaToolsScreenComponent: Component { transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame) - transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size)) return availableSize } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift index 320a81ed35..ea4942a2e6 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerPackListContextItem.swift @@ -110,6 +110,7 @@ private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCu self.scrollNode.view.alwaysBounceVertical = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0) + self.scrollNode.view.scrollsToTop = false } func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift index 0f6a0afce0..89dc8bbf4b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/TintComponent.swift @@ -2,9 +2,9 @@ import Foundation import UIKit import Display import ComponentFlow -import LegacyComponents import MediaEditor import TelegramPresentationData +import SliderComponent private final class TintColorComponent: Component { typealias EnvironmentType = Empty @@ -193,7 +193,7 @@ final class TintComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Tint_Shadows, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .shadows ? .white : UIColor(rgb: 0x808080) ) ), @@ -221,7 +221,7 @@ final class TintComponent: Component { content: AnyComponent( Text( text: component.strings.Story_Editor_Tint_Highlights, - font: Font.regular(14.0), + font: Font.bold(13.0), color: state.section == .highlights ? .white : UIColor(rgb: 0x808080) ) ), @@ -339,32 +339,35 @@ final class TintComponent: Component { let sliderSize = self.slider.update( transition: transition, component: AnyComponent( - AdjustmentSliderComponent( - title: "", - value: state.section == .shadows ? component.shadowsValue.intensity : component.highlightsValue.intensity, - minValue: 0.0, - maxValue: 1.0, - startValue: 0.0, - isEnabled: currentColor != .clear, - trackColor: currentColor != .clear ? currentColor : .white, - displayValue: false, - valueUpdated: { [weak state] value in - if let state { - switch state.section { - case .shadows: - shadowsValueUpdated(state.shadowsValue.withUpdatedIntensity(value)) - case .highlights: - highlightsValueUpdated(state.highlightsValue.withUpdatedIntensity(value)) + SliderComponent( + content: .continuous(SliderComponent.Continuous( + value: CGFloat(state.section == .shadows ? component.shadowsValue.intensity : component.highlightsValue.intensity), + range: 0.0 ... 1.0, + startValue: 0.0, + valueUpdated: { [weak state] value in + if let state { + switch state.section { + case .shadows: + shadowsValueUpdated(state.shadowsValue.withUpdatedIntensity(Float(value))) + case .highlights: + highlightsValueUpdated(state.highlightsValue.withUpdatedIntensity(Float(value))) + } } } - }, + )), + useNative: true, + trackBackgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), + trackForegroundColor: currentColor != .clear ? currentColor : .white, + isEnabled: currentColor != .clear, + trackHeight: 6.0, + displaysBorderOnTracking: true, isTrackingUpdated: { isTracking in isTrackingUpdated(isTracking) } ) ), environment: {}, - containerSize: availableSize + containerSize: CGSize(width: max(0.0, availableSize.width - 28.0 * 2.0), height: 44.0) ) let colorsVerticalSpacing: CGFloat = 9.0 @@ -387,7 +390,8 @@ final class TintComponent: Component { } let verticalSpacing: CGFloat = 3.0 - let sliderFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + highlightsButtonSize.height + verticalSpacing + sizes.first!.height + verticalSpacing), size: sliderSize) + let sliderRowHeight: CGFloat = 52.0 + let sliderFrame = CGRect(origin: CGPoint(x: 28.0, y: topInset + highlightsButtonSize.height + verticalSpacing + sizes.first!.height + verticalSpacing + 7.0), size: sliderSize) if let view = self.slider.view { if view.superview == nil { self.addSubview(view) @@ -395,7 +399,7 @@ final class TintComponent: Component { transition.setFrame(view: view, frame: sliderFrame) } - return CGSize(width: availableSize.width, height: topInset + highlightsButtonSize.height + colorsVerticalSpacing + sizes.first!.height + verticalSpacing + sliderSize.height) + return CGSize(width: availableSize.width, height: topInset + highlightsButtonSize.height + colorsVerticalSpacing + sizes.first!.height + verticalSpacing + sliderRowHeight) } } @@ -407,4 +411,3 @@ final class TintComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } - diff --git a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift index 21b4d08ef8..2099084833 100644 --- a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -257,7 +257,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.closeButton.displaysAsynchronously = false self.rateButton = AudioRateButton() - self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0) + self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -8.0) self.rateButton.displaysAsynchronously = false self.accessibilityAreaNode = AccessibilityAreaNode() @@ -280,7 +280,6 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.scrollNode.addSubnode(self.previousItemNode) self.scrollNode.addSubnode(self.nextItemNode) - self.addSubnode(self.rateButton) self.addSubnode(self.accessibilityAreaNode) self.actionButton.addSubnode(self.playPauseIconNode) @@ -297,6 +296,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.addSubnode(self.actionButton) self.addSubnode(self.closeButton) + self.addSubnode(self.rateButton) self.actionButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -351,6 +351,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.tapRecognizer = tapRecognizer diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 6a5074fdc5..5ad64b0bbb 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -1188,13 +1188,18 @@ public final class MessageInputPanelComponent: Component { let previousPlaceholder = self.component?.placeholder - let defaultInsets = UIEdgeInsets(top: 14.0, left: 9.0, bottom: 6.0, right: 41.0) + let defaultSideInset: CGFloat = component.style == .media ? 16.0 : 9.0 + let defaultInsets = UIEdgeInsets(top: 14.0, left: defaultSideInset, bottom: 6.0, right: 41.0) var insets = defaultInsets let layoutFromTop = component.attachmentButtonMode == .captionDown if let _ = component.attachmentAction { - insets.left = 41.0 + if case .media = component.style { + insets.left = 54.0 + } else { + insets.left = 41.0 + } } if let _ = component.setMediaRecordingActive { insets.right = 41.0 @@ -1202,7 +1207,9 @@ public final class MessageInputPanelComponent: Component { let textFieldSideInset: CGFloat switch component.style { - case .media, .videoChat, .gift: + case .media: + textFieldSideInset = 16.0 + case .videoChat, .gift: textFieldSideInset = 8.0 default: textFieldSideInset = 9.0 @@ -1326,7 +1333,7 @@ public final class MessageInputPanelComponent: Component { let placeholderTransition: ComponentTransition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? ComponentTransition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate let placeholderSize: CGSize - var placeholderColor = UIColor(rgb: 0xffffff, alpha: 0.4) + var placeholderColor = UIColor(rgb: 0xffffff, alpha: 0.35) if case .gift = component.style { placeholderColor = component.theme.chat.inputPanel.inputPlaceholderColor } @@ -1483,11 +1490,9 @@ public final class MessageInputPanelComponent: Component { } var fieldBackgroundFrame: CGRect - if hasMediaRecording { + if hasMediaRecording || [.videoChat, .gift].contains(component.style) { fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: fieldFrame.height)) - } else if [.videoChat, .gift].contains(component.style) { - fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: fieldFrame.height)) - } else if isEditing || component.style == .editor || component.style == .media { + } else if isEditing || [.editor, .media].contains(component.style) { fieldBackgroundFrame = fieldFrame } else { if component.forwardAction != nil && component.likeAction != nil { @@ -1502,10 +1507,8 @@ public final class MessageInputPanelComponent: Component { let rawFieldBackgroundFrame = fieldBackgroundFrame fieldBackgroundFrame.size.height += headerHeight - //transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size)) - switch component.style { - case .gift: + case .media, .editor, .gift, .videoChat: if self.fieldGlassBackgroundView == nil { let fieldGlassBackgroundView = GlassBackgroundView(frame: fieldBackgroundFrame) self.insertSubview(fieldGlassBackgroundView, aboveSubview: self.fieldBackgroundView) @@ -1515,20 +1518,14 @@ public final class MessageInputPanelComponent: Component { self.fieldBackgroundTint.isHidden = true } if let fieldGlassBackgroundView = self.fieldGlassBackgroundView { - fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition) - transition.setFrame(view: fieldGlassBackgroundView, frame: fieldBackgroundFrame) - } - case .videoChat: - if self.fieldGlassBackgroundView == nil { - let fieldGlassBackgroundView = GlassBackgroundView(frame: fieldBackgroundFrame) - self.insertSubview(fieldGlassBackgroundView, aboveSubview: self.fieldBackgroundView) - self.fieldGlassBackgroundView = fieldGlassBackgroundView - - self.fieldBackgroundView.isHidden = true - self.fieldBackgroundTint.isHidden = true - } - if let fieldGlassBackgroundView = self.fieldGlassBackgroundView { - fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: true, tintColor: .init(kind: .custom(style: .default, color: UIColor(rgb: 0x25272e, alpha: 0.72))), transition: transition) + let tintColor: GlassBackgroundView.TintColor + switch component.style { + case .videoChat: + tintColor = .init(kind: .custom(style: .default, color: UIColor(rgb: 0x25272e, alpha: 0.72))) + default: + tintColor = .init(kind: .panel) + } + fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, transition: transition) transition.setFrame(view: fieldGlassBackgroundView, frame: fieldBackgroundFrame) } default: @@ -1540,7 +1537,6 @@ public final class MessageInputPanelComponent: Component { transition.setFrame(view: self.fieldBackgroundTint, frame: fieldBackgroundFrame) transition.setFrame(view: self.mediaRecordingVibrancyContainer, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size)) - //self.fieldBackgroundTint.backgroundColor = .blue transition.setCornerRadius(layer: self.fieldBackgroundTint.layer, cornerRadius: headerHeight > 0.0 ? 18.0 : baseFieldHeight * 0.5) var textClippingFrame = rawFieldBackgroundFrame.offsetBy(dx: 0.0, dy: headerHeight) @@ -1559,9 +1555,9 @@ public final class MessageInputPanelComponent: Component { if isEditing || component.style == .story || component.style == .videoChat || component.style == .gift { placeholderOriginX = 16.0 } else { - placeholderOriginX = floorToScreenPixels(fieldBackgroundFrame.minX + (fieldBackgroundFrame.width - placeholderSize.width) / 2.0) + placeholderOriginX = floorToScreenPixels((fieldBackgroundFrame.width - placeholderSize.width) / 2.0) } - let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: headerHeight + floor((rawFieldBackgroundFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize) + let placeholderFrame = CGRect(origin: CGPoint(x: placeholderOriginX, y: headerHeight + floorToScreenPixels((rawFieldBackgroundFrame.height - placeholderSize.height) * 0.5)), size: placeholderSize) if let placeholderView = self.placeholder.view, let vibrancyPlaceholderView = self.vibrancyPlaceholder.view { if vibrancyPlaceholderView.superview == nil { vibrancyPlaceholderView.layer.anchorPoint = CGPoint() @@ -1900,6 +1896,7 @@ public final class MessageInputPanelComponent: Component { transition: transition, component: AnyComponent(MessageInputActionButtonComponent( mode: attachmentButtonMode, + style: attachmentButtonMode != .attach ? .glass(isTinted: false) : .legacy, storyId: component.storyItem?.id, action: { [weak self] mode, action, sendAction in guard let self, let component = self.component, case .up = action else { @@ -1950,7 +1947,7 @@ public final class MessageInputPanelComponent: Component { } else { attachmentButtonPosition = size.height - insets.bottom - baseFieldHeight + attachmentButtonPosition } - let attachmentButtonFrame = CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5) + (fieldBackgroundFrame.minX - fieldFrame.minX), y: attachmentButtonPosition), size: attachmentButtonSize) + let attachmentButtonFrame = CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5) + (fieldBackgroundFrame.minX - fieldFrame.minX) - 2.0, y: attachmentButtonPosition), size: attachmentButtonSize) transition.setPosition(view: attachmentButtonView, position: attachmentButtonFrame.center) transition.setBounds(view: attachmentButtonView, bounds: CGRect(origin: CGPoint(), size: attachmentButtonFrame.size)) transition.setAlpha(view: attachmentButtonView, alpha: attachmentVisible ? 1.0 : 0.0) diff --git a/submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift b/submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift index 6879454e18..5d175befa2 100644 --- a/submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift +++ b/submodules/TelegramUI/Components/MoreHeaderButton/Sources/MoreHeaderButton.swift @@ -86,7 +86,7 @@ public final class MoreHeaderButton: HighlightableButtonNode { transition: .immediate, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent( - name: "anim_moredots" + name: "anim_morewide" ), color: self.color )), diff --git a/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift index b14965b311..c02b17c648 100644 --- a/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift +++ b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift @@ -316,6 +316,7 @@ public final class MultiplexedVideoNode: ASDisplayNode, ASScrollViewDelegate { } self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false let recognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.view.addGestureRecognizer(recognizer) diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift index 9d217ebf3f..e48b154102 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift @@ -12,8 +12,6 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { return 38.0 } - public static let thinBackArrowImage = generateTintedImage(image: UIImage(bundleImageName: "Navigation/BackArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) - public static let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) var presentationData: NavigationBarPresentationData diff --git a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift index 0c0a659f4d..e33c33a6fa 100644 --- a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift +++ b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift @@ -613,7 +613,7 @@ public func threadNotificationExceptionsScreen(context: AccountContext, peerId: if !state.notificationExceptions.isEmpty { if state.editing { leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}) - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { updateState { value in var value = value value.editing = false diff --git a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift index 0a4c7ffdcc..7df38817e7 100644 --- a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift +++ b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift @@ -1006,11 +1006,11 @@ public func notificationPeerExceptionController( let signal = combineLatest(queue: .mainQueue(), (updatedPresentationData?.signal ?? context.sharedContext.presentationData), context.engine.peers.notificationSoundList(), statePromise.get() |> distinctUntilChanged) |> map { presentationData, notificationSoundList, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { arguments.cancel() }) - let rightNavigationButton = ItemListNavigationButton(content: .text(state.canRemove || edit ? presentationData.strings.Common_Done : presentationData.strings.Notification_Exceptions_Add), style: .bold, enabled: true, action: { + let rightNavigationButton = ItemListNavigationButton(content: state.canRemove || edit ? .icon(.done) : .text(presentationData.strings.Notification_Exceptions_Add), style: .bold, enabled: true, action: { arguments.complete() }) diff --git a/submodules/TelegramUI/Components/OpenUserGeneratedUrl/BUILD b/submodules/TelegramUI/Components/OpenUserGeneratedUrl/BUILD new file mode 100644 index 0000000000..524bf1429b --- /dev/null +++ b/submodules/TelegramUI/Components/OpenUserGeneratedUrl/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "OpenUserGeneratedUrl", + module_name = "OpenUserGeneratedUrl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/OverlayStatusController", + "//submodules/UrlWhitelist", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertWebpagePreviewComponent", + "//submodules/UrlHandling", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/PresentationDataUtils/Sources/OpenUrl.swift b/submodules/TelegramUI/Components/OpenUserGeneratedUrl/Sources/OpenUserGeneratedUrl.swift similarity index 72% rename from submodules/PresentationDataUtils/Sources/OpenUrl.swift rename to submodules/TelegramUI/Components/OpenUserGeneratedUrl/Sources/OpenUserGeneratedUrl.swift index f9c4ba3a1f..d17edb70c6 100644 --- a/submodules/PresentationDataUtils/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Components/OpenUserGeneratedUrl/Sources/OpenUserGeneratedUrl.swift @@ -1,27 +1,46 @@ import Foundation +import UIKit import Display import SwiftSignalKit +import ComponentFlow import TelegramCore import AccountContext import OverlayStatusController import UrlWhitelist import TelegramPresentationData import AlertComponent +import AlertCheckComponent +import AlertWebpagePreviewComponent import UrlHandling -public func openUserGeneratedUrl(context: AccountContext, peerId: EnginePeer.Id?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, forceDark: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise? = nil, alertDisplayUpdated: ((ViewController?) -> Void)? = nil) -> Disposable { +public func openUserGeneratedUrl( + context: AccountContext, + peerId: EnginePeer.Id?, + url: String, + webpage: TelegramMediaWebpage? = nil, + concealed: Bool, + forceConcealed: Bool = false, + skipUrlAuth: Bool = false, + skipConcealedAlert: Bool = false, + forceDark: Bool = false, + present: @escaping (ViewController) -> Void, + openResolved: @escaping (ResolvedUrl) -> Void, + progress: Promise? = nil, + alertDisplayUpdated: ((ViewController?) -> Void)? = nil, + concealedAlertOption: OpenUserGeneratedUrlConcealedAlertOption? = nil +) -> Disposable { var concealed = concealed - + var presentationData = context.sharedContext.currentPresentationData.with { $0 } if forceDark { presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme) } - + let openImpl: () -> Disposable = { let disposable = MetaDisposable() var cancelImpl: (() -> Void)? let progressSignal: Signal - + if let progress { progressSignal = Signal { subscriber in progress.set(.single(true)) @@ -32,7 +51,7 @@ public func openUserGeneratedUrl(context: AccountContext, peerId: EnginePeer.Id? |> runOn(Queue.mainQueue()) } else { progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + let controller = OverlayStatusController(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark, type: .loading(cancelled: { cancelImpl?() })) present(controller) @@ -46,17 +65,17 @@ public func openUserGeneratedUrl(context: AccountContext, peerId: EnginePeer.Id? } let progressDisposable = MetaDisposable() var didStartProgress = false - + cancelImpl = { disposable.dispose() } - + var resolveSignal: Signal resolveSignal = context.sharedContext.resolveUrlWithProgress(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) #if DEBUG //resolveSignal = .single(.progress) |> then(resolveSignal |> delay(2.0, queue: .mainQueue())) #endif - + disposable.set((resolveSignal |> afterDisposed { Queue.mainQueue().async { @@ -75,44 +94,84 @@ public func openUserGeneratedUrl(context: AccountContext, peerId: EnginePeer.Id? openResolved(result) } })) - + return ActionDisposable { cancelImpl?() } } - + let (parsedString, parsedConcealed) = parseUrl(url: url, wasConcealed: concealed) concealed = parsedConcealed - - if let parsedUrl = parseInternalUrl(sharedContext: context.sharedContext, context: context, query: url) { + + if forceConcealed { + concealed = true + } else if let parsedUrl = parseInternalUrl(sharedContext: context.sharedContext, context: context, query: url) { if case .proxy = parsedUrl { concealed = true } } - + if concealed && !skipConcealedAlert { var rawDisplayUrl: String = parsedString let maxLength = 180 if rawDisplayUrl.count > maxLength { rawDisplayUrl = String(rawDisplayUrl[..] = [] + content.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(AlertTitleComponent(title: "Open link?")))) + content.append(AnyComponentWithIdentity(id: "text", component: AnyComponent(AlertTextComponent( + content: .plain(displayUrl), + alignment: .center, + color: .primary, + style: .background(.default), + insets: UIEdgeInsets(top: 9.0, left: 4.0, bottom: 0.0, right: 4.0) + )))) + if let webpage, case .Loaded = webpage.content { + content.append(AnyComponentWithIdentity(id: "webpagePreview", component: AnyComponent(AlertWebpagePreviewComponent( + context: context, + presentationData: presentationData, + webpage: webpage + )))) } - present(alertController) - alertDisplayUpdated?(alertController) + if let concealedAlertOption, let checkState { + content.append(AnyComponentWithIdentity(id: "check", component: AnyComponent(AlertCheckComponent( + title: concealedAlertOption.title, + initialValue: false, + externalState: checkState + )))) + } + + var updatedPresentationDataSignal = context.sharedContext.presentationData + if forceDark { + updatedPresentationDataSignal = .single(presentationData) + } + let controller = AlertScreen( + content: content, + actions: [ + .init(title: presentationData.strings.Common_Cancel), + .init(title: "Open", type: .default, action: { + if checkState?.value == true { + concealedAlertOption?.action() + } + disposable.set(openImpl()) + }), + ], + updatedPresentationData: (presentationData, updatedPresentationDataSignal) + ) + controller.dismissed = { _ in + alertDisplayUpdated?(nil) + } + present(controller) + alertDisplayUpdated?(controller) return disposable } else { return openImpl() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index c35771e65c..79005d5964 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", @@ -121,7 +122,6 @@ swift_library( "//submodules/TranslateUI", "//submodules/UndoUI", "//submodules/MediaPlayer:UniversalMediaPlayer", - "//submodules/WebSearchUI", "//submodules/WebUI", "//submodules/TelegramUI/Components/MultiScaleTextNode", "//submodules/GridMessageSelectionNode", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index 74035b1f59..1bf164df4e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -181,7 +181,8 @@ public final class LoadingOverlayNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, performBotConnectionReviewAction: { _, _ in + }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in }, editPeer: { _ in }, openWebApp: { _ in }, openPhotoSetup: { @@ -521,6 +522,8 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod }, performActiveSessionAction: { _, _ in }, + performBotConnectionReviewAction: { _, _ in + }, openChatFolderUpdates: { }, hideChatFolderUpdates: { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift index 339eb595d4..203de0c50a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGifPaneNode.swift @@ -703,6 +703,7 @@ final class PeerInfoGifPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScrollViewDe } self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) self.addSubnode(self.floatingHeaderNode) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift index f42fd8cfd0..fe4c0b06a0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNavigationButton.swift @@ -13,23 +13,6 @@ private enum MoreIconNodeState: Equatable { case moreToSearch(Float) } -private let glassBackArrowImage: UIImage? = { - let imageSize = CGSize(width: 44.0, height: 44.0) - let topRightPoint = CGPoint(x: 24.6, y: 14.0) - let centerPoint = CGPoint(x: 17.0, y: imageSize.height * 0.5) - return generateImage(imageSize, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor.white.cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setLineJoin(.round) - context.move(to: topRightPoint) - context.addLine(to: centerPoint) - context.addLine(to: CGPoint(x: topRightPoint.x, y: size.height - topRightPoint.y)) - context.strokePath() - })?.withRenderingMode(.alwaysTemplate) -}() - private final class MoreIconNode: ManagedAnimationNode { private let duration: Double = 0.21 private var iconState: MoreIconNodeState = .more @@ -233,7 +216,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { case .back: text = "" accessibilityText = presentationData.strings.Common_Back - icon = glassBackArrowImage + icon = PresentationResourcesRootController.navigationBackIcon(presentationData.theme) case .edit: text = presentationData.strings.Common_Edit accessibilityText = text @@ -271,7 +254,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { case .more: text = "" accessibilityText = presentationData.strings.Common_More - icon = nil// PresentationResourcesRootController.navigationMoreCircledIcon(presentationData.theme) + icon = nil isGestureEnabled = true isAnimation = true animationState = .more 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..51cc9be59f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -29,7 +29,6 @@ import GalleryUI import LegacyUI import MapResourceToAvatarSizes import LegacyComponents -import WebSearchUI import LocationResources import LocationUI import Geocoding @@ -518,9 +517,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) }, @@ -1092,7 +1088,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) }, sendCurrentMessage: { _, _ in - }, sendMessage: { _ in + }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in @@ -1110,8 +1106,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } strongSelf.openUrl(url: url.url, concealed: url.concealed, external: url.external ?? false) }, openExternalInstantPage: { _ in - }, shareCurrentLocation: { - }, shareAccountContact: { + }, shareCurrentLocation: { _ in + }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in guard let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController else { @@ -1166,14 +1162,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro actionSheet?.dismissAnimated() if let strongSelf = self { if canOpenIn { - let actionSheet = OpenInActionSheetController(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: strongSelf.context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { }) } }) strongSelf.view.endEditing(true) - strongSelf.controller?.present(actionSheet, in: .window(.root)) + strongSelf.controller?.push(actionSheet) } else { strongSelf.context.sharedContext.applicationBindings.openUrl(url) } @@ -1226,7 +1222,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in @@ -2960,7 +2956,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(controller) openUrlImpl = { [weak self, weak controller] url, concealed, forceUpdate, commit in - let _ = openUserGeneratedUrl(context: context, peerId: peerId, url: url, concealed: concealed, present: { [weak self] c in + let _ = context.sharedContext.openUserGeneratedUrl(context: context, peerId: peerId, url: url, webpage: nil, concealed: concealed, forceConcealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: false, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, openResolved: { result in var navigationController: NavigationController? @@ -2986,7 +2982,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, dismissInput: { context.sharedContext.mainWindow?.viewController?.view.endEditing(false) }, contentContext: nil, progress: nil, completion: nil) - }) + }, progress: nil, alertDisplayUpdated: nil, concealedAlertOption: nil) } presentImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) @@ -4299,40 +4295,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) { @@ -5421,7 +5384,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var contentHeight: CGFloat = 0.0 let sectionInset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { sectionInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { sectionInset = 0.0 @@ -5827,7 +5790,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if !additive { let sectionInset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { sectionInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { sectionInset = 0.0 @@ -6176,14 +6139,19 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.joinChannelDisposable.set(( self.context.peerChannelMemberCategoriesContextsManager.join(engine: self.context.engine, peerId: peer.id, hash: nil) |> deliverOnMainQueue - |> afterCompleted { [weak self] in - Queue.mainQueue().async { - if let self { - self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Chat_SimilarChannels_JoinedChannel(peer.compactDisplayTitle).string, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } + ).startStrict(next: { [weak self] result in + guard let self else { + return + } + switch result { + case .joined: + self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Chat_SimilarChannels_JoinedChannel(peer.compactDisplayTitle).string, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + case let .webView(webView): + if let controller = self.controller { + self.context.sharedContext.openJoinChatWebView(context: self.context, parentController: controller, updatedPresentationData: self.controller?.updatedPresentationData, webView: webView) } } - ).startStrict(error: { [weak self] error in + }, error: { [weak self] error in guard let self else { return } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift index 5ed112284f..754acfe19a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift @@ -73,7 +73,7 @@ public extension PeerInfoScreenImpl { var avatarPickerHolder: Any? let _ = avatarPickerHolder - let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, peerType: PeerType.getType(for: peer), getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { }, completion: { [weak parentController] result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in avatarPickerHolder = nil @@ -511,7 +511,7 @@ extension PeerInfoScreenImpl { let parentController = (self.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController var dismissImpl: (() -> Void)? - let (mainController, pickerHolder) = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in + let (mainController, pickerHolder) = self.context.sharedContext.makeAvatarMediaPickerScreen(context: self.context, peerType: PeerType.getType(for: peer), getSourceRect: { return nil }, canDelete: hasDeleteButton, performDelete: { [weak self] in self?.openAvatarRemoval(mode: mode, peer: peer, item: item) }, completion: { [weak self] result, transitionView, transitionRect, transitionImage, fromCamera, transitionOut, cancelled in guard let self else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift index 5e1350aaed..6419048931 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMessage.swift @@ -5,11 +5,121 @@ import AccountContext import SwiftSignalKit import TelegramCore import LegacyMediaPickerUI +import MediaPickerUI 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 +223,33 @@ 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 + }, photoToolbarView: { [context = strongSelf.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) + }, 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/PeerInfoScreenOpenURL.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift index 270236ed71..858c399463 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift @@ -52,7 +52,7 @@ extension PeerInfoScreenNode { } func openUrl(url: String, concealed: Bool, external: Bool, forceExternal: Bool = false, commit: @escaping () -> Void = {}) { - let _ = openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in + let _ = self.context.sharedContext.openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, webpage: nil, concealed: concealed, forceConcealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: false, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, openResolved: { [weak self] tempResolved in guard let strongSelf = self else { @@ -76,17 +76,17 @@ extension PeerInfoScreenNode { }, dismissInput: { self?.view.endEditing(true) }, contentContext: nil, progress: nil, completion: nil) - }) + }, progress: nil, alertDisplayUpdated: nil, concealedAlertOption: nil) } func openUrlIn(_ url: String) { - let actionSheet = OpenInActionSheetController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { }) } }) - self.controller?.present(actionSheet, in: .window(.root)) + self.controller?.push(actionSheet) } func openPeerMention(_ name: String, navigation: ChatControllerInteractionNavigateToPeer = .default) { 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/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index b3d43bb29d..b51b155ebe 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -251,12 +251,12 @@ public final class StorySearchGridScreen: ViewControllerComponentContainer { } self.present(self.context.sharedContext.makeShareController(context: self.context, params: ShareControllerParams(subject: .mapMedia(locationMap), externalShare: true)), in: .window(.root), with: nil) }) - self.present(OpenInActionSheetController(context: self.context, updatedPresentationData: nil, item: .location(location: locationMap, directions: nil), additionalAction: shareAction, openUrl: { [weak self] url in + self.push(OpenInOptionsScreen(context: self.context, updatedPresentationData: nil, item: .location(location: locationMap, directions: nil), additionalAction: shareAction, openUrl: { [weak self] url in guard let self else { return } self.context.sharedContext.applicationBindings.openUrl(url) - }), in: .window(.root), with: nil) + })) } func updateTitle() { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift index bd468e9915..fe41b91c02 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/AddGiftsScreen.swift @@ -11,9 +11,7 @@ import AccountContext import ComponentFlow import ViewControllerComponent import BundleIconComponent -import MultilineTextComponent -import ButtonComponent -import BlurredBackgroundComponent +import BottomButtonPanelComponent import ContextUI final class AddGiftsScreenComponent: Component { @@ -55,9 +53,8 @@ final class AddGiftsScreenComponent: Component { private var giftsListView: GiftsListView? - private let buttonBackground = ComponentView() - private let buttonSeparator = SimpleLayer() - private let button = ComponentView() + private let buttonPanel = ComponentView() + private var bottomPanelHeight: CGFloat = 68.0 private var isUpdating: Bool = false @@ -113,7 +110,7 @@ final class AddGiftsScreenComponent: Component { var contentSize = CGSize(width: self.scrollView.bounds.width, height: contentHeight) contentSize.height += environment.safeInsets.bottom contentSize.height = max(contentSize.height, self.scrollView.bounds.size.height) - contentSize.height += 50.0 + 24.0 + contentSize.height += self.bottomPanelHeight transition.setFrame(view: giftsListView, frame: CGRect(origin: CGPoint(), size: contentSize)) if self.scrollView.contentSize != contentSize { @@ -160,47 +157,15 @@ final class AddGiftsScreenComponent: Component { self.state = state let sideInset: CGFloat = 16.0 + environment.safeInsets.left - let buttonHeight: CGFloat = 50.0 - let bottomPanelPadding: CGFloat = 12.0 - let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding - let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset - - let bottomPanelOffset: CGFloat = giftsListView.selectedItems.count > 0 ? 0.0 : bottomPanelHeight - - let buttonString = environment.strings.AddGifts_AddGifts(Int32(giftsListView.selectedItems.count)) - let bottomPanelSize = self.buttonBackground.update( + let buttonString = environment.strings.AddGifts_AddGifts(Int32(max(1, giftsListView.selectedItems.count))) + let bottomPanelSize = self.buttonPanel.update( transition: transition, - component: AnyComponent(BlurredBackgroundComponent( - color: environment.theme.rootController.tabBar.backgroundColor - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight) - ) - self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor - - if let view = self.buttonBackground.view { - if view.superview == nil { - self.addSubview(view) - self.layer.addSublayer(self.buttonSeparator) - } - transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: bottomPanelSize)) - transition.setFrame(layer: self.buttonSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - } - - let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) - let buttonSize = self.button.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), - cornerRadius: 10.0 - ), - content: AnyComponentWithIdentity( - id: AnyHashable(buttonAttributedString.string), - component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) - ), + component: AnyComponent(BottomButtonPanelComponent( + theme: environment.theme, + title: buttonString, + label: nil, + isEnabled: true, + insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset), action: { [weak self] in guard let self, let controller = self.environment?.controller() as? AddGiftsScreen, let giftsListView = self.giftsListView else { return @@ -210,18 +175,21 @@ final class AddGiftsScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight) + containerSize: availableSize ) - if let buttonView = self.button.view { - if buttonView.superview == nil { - self.addSubview(buttonView) + self.bottomPanelHeight = bottomPanelSize.height + + let bottomPanelOffset: CGFloat = giftsListView.selectedItems.count > 0 ? 0.0 : bottomPanelSize.height + if let buttonPanelView = self.buttonPanel.view { + if buttonPanelView.superview == nil { + self.addSubview(buttonPanelView) } - transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding + bottomPanelOffset), size: buttonSize)) + transition.setFrame(view: buttonPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: bottomPanelSize)) } let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0) let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - let _ = giftsListView.update(size: availableSize, sideInset: 0.0, bottomInset: max(environment.safeInsets.bottom, bottomPanelHeight), deviceMetrics: environment.deviceMetrics, visibleHeight: availableSize.height, isScrollingLockedAtTop: false, expandProgress: 0.0, presentationData: presentationData, synchronous: false, visibleBounds: visibleBounds, transition: transition.containedViewLayoutTransition) + let _ = giftsListView.update(size: availableSize, sideInset: 0.0, bottomInset: max(environment.safeInsets.bottom, bottomPanelSize.height), deviceMetrics: environment.deviceMetrics, visibleHeight: availableSize.height, isScrollingLockedAtTop: false, expandProgress: 0.0, presentationData: presentationData, synchronous: false, visibleBounds: visibleBounds, transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: availableSize)) self.backgroundView.backgroundColor = environment.theme.list.blocksBackgroundColor @@ -298,7 +266,7 @@ public final class AddGiftsScreen: ViewControllerComponentContainer { } self.filterButton.addTarget(self, action: #selector(self.filterPressed), forControlEvents: .touchUpInside) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.filterButton) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift index 133c4006e9..e1c98a1ac3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift @@ -819,12 +819,13 @@ final class GiftsListView: UIView { environment: {}, containerSize: CGSize(width: params.size.width - sideInset * 2.0, height: params.size.height) ) - let buttonAttributedString = NSAttributedString(string: presentationData.strings.PeerInfo_Gifts_EmptyCollection_Action, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + let buttonAttributedString = NSAttributedString(string: presentationData.strings.PeerInfo_Gifts_EmptyCollection_Action, font: Font.semibold(17.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let emptyResultsActionSize = self.emptyResultsAction.update( transition: .immediate, component: AnyComponent( ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: presentationData.theme.list.itemCheckColors.fillColor, foreground: presentationData.theme.list.itemCheckColors.foregroundColor, pressedColor: presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) @@ -840,7 +841,7 @@ final class GiftsListView: UIView { ) ), environment: {}, - containerSize: CGSize(width: 240.0, height: 50.0) + containerSize: CGSize(width: 240.0, height: 52.0) ) let emptyTotalHeight = emptyResultsTitleSize.height + emptyTextSpacing + emptyResultsTextSize.height + emptyTextSpacing + emptyResultsActionSize.height diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 1e7f1d5365..ee9ea2d9fc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -212,6 +212,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.scrollNode.view.contentInsetAdjustmentBehavior = .never self.scrollNode.view.delegate = self + self.scrollNode.view.scrollsToTop = false if let tabSelectorView = self.tabSelector.view { self.scrollNode.view.insertSubview(self.giftsListView, aboveSubview: tabSelectorView) @@ -783,7 +784,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr buttonTitle = params.presentationData.strings.PeerInfo_Gifts_SendGift } - let buttonAttributedString = NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + let buttonAttributedString = NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: presentationData.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) var buttonTitleContent: AnyComponent = AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) if let buttonIconName { buttonTitleContent = AnyComponent(HStack([ diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 367f40270f..921303febf 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -1526,7 +1526,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr var address: String? var distance: Double? var drivingTime: ExpectedTravelTime - var transitTime: ExpectedTravelTime var walkingTime: ExpectedTravelTime var hasEta: Bool @@ -1535,7 +1534,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr address: String?, distance: Double?, drivingTime: ExpectedTravelTime, - transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, hasEta: Bool ) { @@ -1543,7 +1541,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr self.address = address self.distance = distance self.drivingTime = drivingTime - self.transitTime = transitTime self.walkingTime = walkingTime self.hasEta = hasEta } @@ -1561,7 +1558,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr private let contextGestureContainerNode: ContextControllerSourceNode - private var mapOptionsNode: LocationOptionsNode? private var mapNode: LocationMapHeaderNode? private var mapDisposable: Disposable? @@ -2191,7 +2187,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if case .location = scope { let mapNode = LocationMapHeaderNode( presentationData: self.presentationData, - glass: false, + glass: true, toggleMapModeSelection: { [weak self] in guard let self else { return @@ -2201,7 +2197,15 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr state.displayingMapModeOptions = !state.displayingMapModeOptions self.locationViewState = state }, - updateMapMode: { _ in + updateMapMode: { [weak self] mode in + guard let self else { + return + } + + var state = self.locationViewState + state.mapMode = mode + state.displayingMapModeOptions = false + self.locationViewState = state }, goToUserLocation: { [weak self] in guard let self else { @@ -2333,7 +2337,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr strongSelf.itemGridBinding.updatePresentationData(presentationData: presentationData) strongSelf.itemGrid.updatePresentationData(theme: presentationData.theme) - strongSelf.mapOptionsNode?.updatePresentationData(presentationData) }) self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false) @@ -2354,24 +2357,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr throttledUserLocation(mapNode.mapNode.userLocation) ) - var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating)) + var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating)) var address: Signal = .single(nil) let locale = localeWithStrings(self.presentationData.strings) - eta = .single((.calculating, .calculating, .calculating)) - |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .automobile), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .transit), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .walking)) - |> mapToSignal { drivingTime, transitTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> in + eta = .single((.calculating, .calculating)) + |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .automobile), getExpectedTravelTime(coordinate: locationCoordinate, transportType: .walking)) + |> mapToSignal { drivingTime, walkingTime -> Signal<(ExpectedTravelTime, ExpectedTravelTime), NoError> in if case .calculating = drivingTime { return .complete() } - if case .calculating = transitTime { - return .complete() - } if case .calculating = walkingTime { return .complete() } - return .single((drivingTime, transitTime, walkingTime)) + return .single((drivingTime, walkingTime)) }) /*if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty { @@ -2416,8 +2416,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr address: address, distance: distance, drivingTime: eta.0, - transitTime: eta.1, - walkingTime: eta.2, + walkingTime: eta.1, hasEta: false ) @@ -3755,12 +3754,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr address: addressString, distance: distanceString, drivingTime: mapInfoData.drivingTime, - transitTime: mapInfoData.transitTime, walkingTime: mapInfoData.walkingTime, hasEta: mapInfoData.hasEta, action: {}, drivingAction: {}, - transitAction: {}, walkingAction: {} ) let (mapInfoLayout, mapInfoReadyAndApply) = mapInfoNode.asyncLayout()( @@ -4200,30 +4197,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if self.mapNode != nil { self.updateMapLayout(size: size, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, transition: transition) gridTopInset += self.effectiveMapHeight - - let mapOptionsNode: LocationOptionsNode - if let current = self.mapOptionsNode { - mapOptionsNode = current - } else { - mapOptionsNode = LocationOptionsNode(presentationData: self.presentationData, hasBackground: false, updateMapMode: { [weak self] mode in - guard let self else { - return - } - - var state = self.locationViewState - state.mapMode = mode - state.displayingMapModeOptions = false - self.locationViewState = state - }) - mapOptionsNode.clipsToBounds = true - self.mapOptionsNode = mapOptionsNode - self.parentController?.navigationBar?.additionalContentNode.addSubnode(mapOptionsNode) - } - - let mapOptionsFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - self.additionalNavigationHeight), size: CGSize(width: size.width, height: self.additionalNavigationHeight)) - transition.updatePosition(node: mapOptionsNode, position: mapOptionsFrame.center) - transition.updateBounds(node: mapOptionsNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: 38.0 - self.additionalNavigationHeight), size: mapOptionsFrame.size)) - mapOptionsNode.updateLayout(size: mapOptionsFrame.size, leftInset: sideInset, rightInset: sideInset, transition: transition) } var hasBarBackground = false diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift index 09747a6867..7d549c6c61 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift @@ -102,8 +102,7 @@ private final class SheetContent: Component { self.environment = environment var contentHeight: CGFloat = 0.0 - - let titleString: String = environment.strings.ProfileLevelInfo_Title + let descriptionTextString: String var secondaryDescriptionTextString: String? if component.peer.id == component.context.account.peerId { @@ -178,7 +177,6 @@ private final class SheetContent: Component { )) } - let _ = titleString let titleSize = self.title.update( transition: transition, component: AnyComponent(AnimatedTextComponent( diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift index db24d5d6ca..4a3f69ff6c 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift @@ -307,7 +307,7 @@ public func oldChannelsController(context: AccountContext, updatedPresentationDa ) ) |> map { presentationData, state, peers, limits -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .icon(.close), style: .regular, enabled: true, action: { dismissImpl?() }) let (accountPeer, limits, premiumLimits) = limits diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 1d21f6dc9a..37a7ccd34c 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -700,7 +700,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } - let controller = chatTextLinkEditController(context: context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link in + let controller = chatTextLinkEditController(context: context, updatedPresentationData: (presentationData, .never()), text: presentationData.strings.TextFormat_AddLinkText(text?.string ?? "").string, link: link, apply: { [weak self] link, _ in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link = link { strongSelf.updateChatPresentationInterfaceState(animated: true, { state in @@ -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/SaveProgressScreen/BUILD b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD index e9de562525..9d05ce1532 100644 --- a/submodules/TelegramUI/Components/SaveProgressScreen/BUILD +++ b/submodules/TelegramUI/Components/SaveProgressScreen/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/Components/BundleIconComponent", "//submodules/Components/LottieAnimationComponent", "//submodules/AccountContext", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", 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..6fec00f75a 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD @@ -23,7 +23,10 @@ swift_library( "//submodules/AppBundle", "//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..a4e86a5cbe 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,64 @@ import AnimatedAvatarSetNode import AvatarNode import TelegramStringFormatting import ChatMessagePaymentAlertController +import ResizableSheetComponent +import ButtonComponent +import BundleIconComponent +import GlassBarButtonComponent +import MultilineTextComponent -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 +82,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 +102,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 +149,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 +162,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 +202,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 +229,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 +270,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 +300,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 +321,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,57 +329,50 @@ private final class SendInviteLinkScreenComponent: Component { contentHeight += premiumTextSize.height contentHeight += 22.0 - let premiumButtonTitle = environment.strings.SendInviteLink_SubscribeToPremiumButton + let premiumButtonGradientColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] let premiumButtonSize = premiumButton.update( - transition: transition, - component: AnyComponent(SolidRoundedButtonComponent( - title: premiumButtonTitle, - badge: nil, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: .black, - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + transition: premiumItemsTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: premiumButtonGradientColors[0], + foreground: .white, + pressedColor: premiumButtonGradientColors[0], + isShimmering: false, + gradient: ButtonComponent.Background.Gradient(colors: premiumButtonGradientColors) ), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 11.0, - gloss: false, - 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) + content: AnyComponentWithIdentity( + id: AnyHashable(environment.strings.SendInviteLink_SubscribeToPremiumButton), + component: AnyComponent(ButtonTextContentComponent( + text: environment.strings.SendInviteLink_SubscribeToPremiumButton, + badge: 0, + textColor: .white, + badgeBackground: .white, + badgeForeground: premiumButtonGradientColors[0] + )) + ), + action: { + component.openPremium() } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 52.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 +387,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 +396,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 +457,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 +474,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 +485,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 +507,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 +537,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 +550,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 +559,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 +643,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 +663,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 +1168,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 +1222,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 +1247,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/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index cf6436f827..5de66a15b3 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -196,6 +196,8 @@ final class GreetingMessageListItemComponent: Component { }, performActiveSessionAction: { _, _ in }, + performBotConnectionReviewAction: { _, _ in + }, openChatFolderUpdates: { }, hideChatFolderUpdates: { 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/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index a2dd3ce948..2923c0bd2e 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -211,6 +211,8 @@ final class QuickReplySetupScreenComponent: Component { }, performActiveSessionAction: { _, _ in }, + performBotConnectionReviewAction: { _, _ in + }, openChatFolderUpdates: { }, hideChatFolderUpdates: { @@ -767,20 +769,13 @@ final class QuickReplySetupScreenComponent: Component { titleText = strings.QuickReply_Title } - let closeTitle: String - switch component.mode { - case .manage: - closeTitle = strings.Common_Close - case .select: - closeTitle = strings.Common_Cancel - } let headerContent: ChatListHeaderComponent.Content? = ChatListHeaderComponent.Content( title: titleText, navigationBackTitle: nil, titleComponent: nil, chatListTitle: nil, leftButton: isModal ? AnyComponentWithIdentity(id: "close", component: AnyComponent(NavigationButtonComponent( - content: .text(title: closeTitle, isBold: false), + content: .icon(imageName: "Navigation/Close"), pressed: { [weak self] _ in guard let self else { return 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/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index 82307c7d53..3e66115baf 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -219,7 +219,13 @@ final class BusinessLocationSetupScreenComponent: Component { guard let component = self.component else { return } - let controller = LocationPickerController(context: component.context, updatedPresentationData: nil, mode: .pick, initialLocation: initialLocation, completion: { [weak self] location, _, _, address, _ in + let controller = LocationPickerController( + context: component.context, + style: .glass, + updatedPresentationData: nil, + mode: .pick, + initialLocation: initialLocation, + completion: { [weak self] location, _, _, address, _ in guard let self else { return } @@ -521,6 +527,7 @@ final class BusinessLocationSetupScreenComponent: Component { items: [ AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, + style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( @@ -609,7 +616,7 @@ final class BusinessLocationSetupScreenComponent: Component { if let current = self.applyButtonItem { applyButtonItem = current } else { - applyButtonItem = UIBarButtonItem(title: environment.strings.Common_Save, style: .done, target: self, action: #selector(self.savePressed)) + applyButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.savePressed)) } if controller.navigationItem.rightBarButtonItem !== applyButtonItem { controller.navigationItem.setRightBarButton(applyButtonItem, animated: true) diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift index 17f2df3631..7e56f86bef 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift @@ -655,7 +655,31 @@ final class BusinessRecipientListScreenComponent: Component { } } -final class BusinessRecipientListScreen: ViewControllerComponentContainer { +public final class BusinessRecipientListScreen: ViewControllerComponentContainer { + public struct ResolvedPeer { + public let peer: EnginePeer + public let isContact: Bool + + public init(peer: EnginePeer, isContact: Bool) { + self.peer = peer + self.isContact = isContact + } + } + + public struct PeerListState { + public let categories: TelegramBusinessRecipients.Categories + public let peers: [ResolvedPeer] + public let excludePeers: [ResolvedPeer] + public let excludeByDefault: Bool + + public init(categories: TelegramBusinessRecipients.Categories, peers: [ResolvedPeer], excludePeers: [ResolvedPeer], excludeByDefault: Bool) { + self.categories = categories + self.peers = peers + self.excludePeers = excludePeers + self.excludeByDefault = excludeByDefault + } + } + final class InitialData { fileprivate let peerList: BusinessRecipientListScreenComponent.PeerList @@ -666,7 +690,7 @@ final class BusinessRecipientListScreen: ViewControllerComponentContainer { } } - enum Mode { + public enum Mode { case includeExceptions case excludeExceptions case excludeUsers @@ -711,6 +735,121 @@ final class BusinessRecipientListScreen: ViewControllerComponentContainer { deinit { } + public static func openSetupFlow(context: AccountContext, from controller: ViewController, state: PeerListState, isExclude: Bool, update: @escaping (PeerListState) -> Void) { + let mode: BusinessRecipientListScreen.Mode + if isExclude { + mode = .excludeUsers + } else if state.excludeByDefault { + mode = .excludeExceptions + } else { + mode = .includeExceptions + } + + let mappedPeerList = BusinessRecipientListScreenComponent.PeerList( + categories: isExclude ? Set() : self.mapCategories(state.categories), + peers: (isExclude ? state.excludePeers : state.peers).map { peer in + BusinessRecipientListScreenComponent.PeerList.Peer( + peer: peer.peer, + isContact: peer.isContact + ) + } + ) + + let applyUpdate: (BusinessRecipientListScreenComponent.PeerList) -> PeerListState = { updatedPeerList in + var updatedState = state + + switch mode { + case .excludeExceptions, .includeExceptions: + updatedState = PeerListState( + categories: self.mapCategories(updatedPeerList.categories), + peers: updatedPeerList.peers.map { peer in + ResolvedPeer(peer: peer.peer, isContact: peer.isContact) + }, + excludePeers: state.excludePeers.filter { excludePeer in + !updatedPeerList.peers.contains(where: { $0.peer.id == excludePeer.peer.id }) + }, + excludeByDefault: state.excludeByDefault + ) + case .excludeUsers: + let excludePeers = updatedPeerList.peers.map { peer in + ResolvedPeer(peer: peer.peer, isContact: peer.isContact) + } + updatedState = PeerListState( + categories: state.categories, + peers: state.peers.filter { peer in + !excludePeers.contains(where: { $0.peer.id == peer.peer.id }) + }, + excludePeers: excludePeers, + excludeByDefault: state.excludeByDefault + ) + } + + return updatedState + } + + if mappedPeerList.categories.isEmpty && mappedPeerList.peers.isEmpty { + let setupController = BusinessRecipientListScreenComponent.View.makePeerListSetupScreen( + context: context, + mode: mode, + initialPeerList: mappedPeerList, + completion: { peerList in + controller.push(BusinessRecipientListScreen( + context: context, + peerList: peerList, + mode: mode, + update: { updatedPeerList in + update(applyUpdate(updatedPeerList)) + } + )) + } + ) + controller.push(setupController) + } else { + controller.push(BusinessRecipientListScreen( + context: context, + peerList: mappedPeerList, + mode: mode, + update: { updatedPeerList in + update(applyUpdate(updatedPeerList)) + } + )) + } + } + + private static func mapCategories(_ categories: TelegramBusinessRecipients.Categories) -> Set { + var mappedCategories = Set() + if categories.contains(.existingChats) { + mappedCategories.insert(.existingChats) + } + if categories.contains(.newChats) { + mappedCategories.insert(.newChats) + } + if categories.contains(.contacts) { + mappedCategories.insert(.contacts) + } + if categories.contains(.nonContacts) { + mappedCategories.insert(.nonContacts) + } + return mappedCategories + } + + private static func mapCategories(_ categories: Set) -> TelegramBusinessRecipients.Categories { + var mappedCategories: TelegramBusinessRecipients.Categories = [] + if categories.contains(.existingChats) { + mappedCategories.insert(.existingChats) + } + if categories.contains(.newChats) { + mappedCategories.insert(.newChats) + } + if categories.contains(.contacts) { + mappedCategories.insert(.contacts) + } + if categories.contains(.nonContacts) { + mappedCategories.insert(.nonContacts) + } + return mappedCategories + } + @objc private func cancelPressed() { self.dismiss() } diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index b31513a97f..0b14eda092 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -825,20 +825,21 @@ final class ChatbotSetupScreenComponent: Component { self.botResolutionState = botResolutionState self.botRights = [.reply, .readMessages, .deleteSentMessages, .deleteReceivedMessages] self.state?.updated(transition: .spring(duration: 0.3)) + + controller.present(UndoOverlayController( + presentationData: presentationData, + content: .actionSucceeded(title: nil, text: environment.strings.ChatbotSetup_BotInstalled(peer.compactDisplayTitle).string, cancel: nil, destructive: false), + elevatedLayout: false, + position: .bottom, + animateInAsReplacement: false, + action: { _ in return true } + ), in: .current) } else { self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: presentationData.strings.ChatbotSetup_ErrorBotNotBusinessCapable, actions: [ TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { }) ]), in: .window(.root)) } - 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), - elevatedLayout: false, - position: .bottom, - animateInAsReplacement: false, - action: { _ in return true } - ), in: .current) } }, removeAction: { [weak self] in diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift index 635a2eb95d..3de44cb9bf 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreenNode.swift @@ -451,7 +451,7 @@ final class LanguageSelectionScreenNode: ViewControllerTracingNode { var listInsets = layout.insets(options: [.input]) listInsets.top += navigationBarHeight - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) listInsets.left += inset listInsets.right += inset diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift index 46c32e6195..904dcee8e4 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/ChannelAppearanceScreen.swift @@ -169,7 +169,8 @@ final class ChannelAppearanceScreenComponent: Component { } final class View: UIView, UIScrollViewDelegate { - private let edgeEffectView: EdgeEffectView + private let topEdgeEffectView: EdgeEffectView + private let bottomEdgeEffectView: EdgeEffectView private let topOverscrollLayer = SimpleLayer() private let scrollView: ScrollView private let actionButton = ComponentView() @@ -225,7 +226,8 @@ final class ChannelAppearanceScreenComponent: Component { private weak var emojiStatusSelectionController: ViewController? override init(frame: CGRect) { - self.edgeEffectView = EdgeEffectView() + self.topEdgeEffectView = EdgeEffectView() + self.bottomEdgeEffectView = EdgeEffectView() self.scrollView = ScrollView() self.scrollView.showsVerticalScrollIndicator = true @@ -246,7 +248,8 @@ final class ChannelAppearanceScreenComponent: Component { self.scrollView.layer.addSublayer(self.topOverscrollLayer) - self.addSubview(self.edgeEffectView) + self.addSubview(self.topEdgeEffectView) + self.addSubview(self.bottomEdgeEffectView) } required init?(coder: NSCoder) { @@ -339,7 +342,7 @@ final class ChannelAppearanceScreenComponent: Component { transition.setAlpha(view: navigationTitleView, alpha: navigationAlpha) } - //transition.setAlpha(view: self.edgeEffectView, alpha: navigationAlpha) + transition.setAlpha(view: self.topEdgeEffectView, alpha: navigationAlpha) } private func resolveState() -> ResolvedState? { @@ -1823,10 +1826,15 @@ final class ChannelAppearanceScreenComponent: Component { transition.setAlpha(view: buttonView, alpha: 1.0) } - let edgeEffectHeight: CGFloat = availableSize.height - buttonY + 36.0 - let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - edgeEffectHeight), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) - transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) - self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, alpha: 1.0, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: transition) + let topEdgeEffectHeight: CGFloat = environment.navigationHeight + 24.0 + let topEdgeEffectFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topEdgeEffectHeight)) + transition.setFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame) + self.topEdgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, alpha: 1.0, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: transition) + + let bottomEdgeEffectHeight: CGFloat = availableSize.height - buttonY + 36.0 + let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomEdgeEffectHeight), size: CGSize(width: availableSize.width, height: bottomEdgeEffectHeight)) + transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame) + self.bottomEdgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: transition) let previousBounds = self.scrollView.bounds diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index a0dac353e4..b3a1c22249 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -30,7 +30,6 @@ import BundleIconComponent import Markdown import PeerNameColorItem import EmojiActionIconComponent -import TabSelectorComponent import WallpaperResources import EdgeEffect import TextFormat @@ -65,14 +64,11 @@ final class UserAppearanceScreenComponent: Component { } let context: AccountContext - let overNavigationContainer: UIView init( - context: AccountContext, - overNavigationContainer: UIView + context: AccountContext ) { self.context = context - self.overNavigationContainer = overNavigationContainer } static func ==(lhs: UserAppearanceScreenComponent, rhs: UserAppearanceScreenComponent) -> Bool { @@ -175,15 +171,27 @@ final class UserAppearanceScreenComponent: Component { private let scrollView: ScrollView private let actionButton = ComponentView() private let edgeEffectView: EdgeEffectView - - private let tabSelector = ComponentView() + enum Section: Int32 { case profile case name + + init(segmentIndex: Int) { + self = segmentIndex == 1 ? .name : .profile + } + + var segmentIndex: Int { + switch self { + case .profile: + return 0 + case .name: + return 1 + } + } } private var currentSection: Section = .profile - private let previewShadowView = UIImageView(image: generatePreviewShadowImage()) + private let previewEdgeEffectView = EdgeEffectView() private let profilePreview = ComponentView() private let profileColorSection = ComponentView() @@ -258,6 +266,7 @@ final class UserAppearanceScreenComponent: Component { self.edgeEffectView = EdgeEffectView() self.edgeEffectView.isUserInteractionEnabled = false + self.previewEdgeEffectView.isUserInteractionEnabled = false super.init(frame: frame) @@ -268,7 +277,7 @@ final class UserAppearanceScreenComponent: Component { self.scrollView.layer.addSublayer(self.topOverscrollLayer) - self.containerView.addSubview(self.previewShadowView) + self.containerView.addSubview(self.previewEdgeEffectView) self.addSubview(self.edgeEffectView) } @@ -328,6 +337,31 @@ final class UserAppearanceScreenComponent: Component { return true } + private func prepareForSectionSwitch(to updatedSection: Section) { + if (updatedSection == .name && self.selectedProfileGift != nil) || (updatedSection == .profile && self.selectedNameGift != nil) { + switch updatedSection { + case .profile: + self.selectedNameGift = nil + self.updatedPeerNameColor = nil + self.updatedPeerNameEmoji = nil + case .name: + self.selectedProfileGift = nil + self.updatedPeerProfileColor = nil + self.updatedPeerProfileEmoji = nil + self.updatedPeerStatus = nil + } + } + } + + func switchToSection(_ updatedSection: Section, transition: ComponentTransition = .easeInOut(duration: 0.3)) { + if self.currentSection == updatedSection { + return + } + self.prepareForSectionSwitch(to: updatedSection) + self.currentSection = updatedSection + self.state?.updated(transition: transition.withUserData(TransitionHint(animateTabChange: true))) + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { self.updateScrolling(transition: .immediate) } @@ -1007,26 +1041,18 @@ final class UserAppearanceScreenComponent: Component { self.environment = environment if self.component == nil { - if let controller = environment.controller() as? UserAppearanceScreen, let focusOnItemTag = controller.focusOnItemTag { - switch focusOnItemTag { - case .profile: - self.currentSection = .profile - case .profileAddIcons: - self.currentSection = .profile - Queue.mainQueue().after(0.1) { - self.openEmojiSetup() + if let controller = environment.controller() as? UserAppearanceScreen { + self.currentSection = controller.selectedSection + + if let focusOnItemTag = controller.focusOnItemTag { + switch focusOnItemTag { + case .profileAddIcons, .nameAddIcons: + Queue.mainQueue().after(0.1) { + self.openEmojiSetup() + } + default: + break } - case .profileUseGift: - self.currentSection = .profile - case .name: - self.currentSection = .name - case .nameAddIcons: - self.currentSection = .name - Queue.mainQueue().after(0.1) { - self.openEmojiSetup() - } - case .nameUseGift: - self.currentSection = .name } } } @@ -1034,7 +1060,9 @@ final class UserAppearanceScreenComponent: Component { self.component = component self.state = state - transition.setFrame(view: component.overNavigationContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: environment.navigationHeight))) + if let controller = environment.controller() as? UserAppearanceScreen { + controller.updateSegmentedTitleView(theme: environment.theme, strings: environment.strings) + } let theme = environment.theme @@ -1164,61 +1192,6 @@ final class UserAppearanceScreenComponent: Component { previewTransition = .immediate } - let tabSelectorSize = self.tabSelector.update( - transition: transition, - component: AnyComponent( - TabSelectorComponent( - colors: TabSelectorComponent.Colors( - foreground: environment.theme.list.itemAccentColor, - selection: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), - normal: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.78), - simple: true - ), - theme: environment.theme, - customLayout: TabSelectorComponent.CustomLayout(font: Font.semibold(16.0)), - items: [ - TabSelectorComponent.Item(id: Section.profile.rawValue, title: environment.strings.ProfileColorSetup_TitleProfile), - TabSelectorComponent.Item(id: Section.name.rawValue, title: environment.strings.ProfileColorSetup_TitleName) - ], - selectedId: self.currentSection.rawValue, - setSelectedId: { [weak self] value in - guard let self else { - return - } - if let intValue = value.base as? Int32 { - let updatedSection = Section(rawValue: intValue) ?? .profile - if self.currentSection != updatedSection { - if (updatedSection == .name && self.selectedProfileGift != nil) || (updatedSection == .profile && self.selectedNameGift != nil) { - switch updatedSection { - case .profile: - self.selectedNameGift = nil - self.updatedPeerNameColor = nil - self.updatedPeerNameEmoji = nil - case .name: - self.selectedProfileGift = nil - self.updatedPeerProfileColor = nil - self.updatedPeerProfileEmoji = nil - self.updatedPeerStatus = nil - } - } - self.currentSection = updatedSection - self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true))) - } - } - } - ) - ), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 44.0) - ) - let tabSelectorFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - tabSelectorSize.width) / 2.0), y: environment.statusBarHeight + 2.0 + floorToScreenPixels((environment.navigationHeight - environment.statusBarHeight - tabSelectorSize.height) / 2.0)), size: tabSelectorSize) - if let tabSelectorView = self.tabSelector.view { - if tabSelectorView.superview == nil { - component.overNavigationContainer.addSubview(tabSelectorView) - } - transition.setFrame(view: tabSelectorView, frame: tabSelectorFrame) - } - let bottomContentInset: CGFloat = 24.0 let bottomInset: CGFloat = 8.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left @@ -1229,8 +1202,7 @@ final class UserAppearanceScreenComponent: Component { var contentHeight: CGFloat = 0.0 let itemCornerRadius: CGFloat = 26.0 - - transition.setTintColor(view: self.previewShadowView, color: environment.theme.list.itemBlocksBackgroundColor) + let previewEdgeEffectHeight: CGFloat = 72.0 switch self.currentSection { case .profile: @@ -1281,8 +1253,10 @@ final class UserAppearanceScreenComponent: Component { } contentHeight += profilePreviewSize.height - 38.0 - transition.setFrame(view: self.previewShadowView, frame: profilePreviewFrame.insetBy(dx: -45.0, dy: -45.0)) - previewTransition.setAlpha(view: self.previewShadowView, alpha: !self.scrolledUp ? 1.0 : 0.0) + let previewEdgeEffectFrame = CGRect(origin: CGPoint(x: profilePreviewFrame.minX, y: profilePreviewFrame.maxY - 35.0), size: CGSize(width: profilePreviewFrame.width, height: previewEdgeEffectHeight)) + previewTransition.setFrame(view: self.previewEdgeEffectView, frame: previewEdgeEffectFrame) + self.previewEdgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, blur: true, alpha: 1.0, rect: previewEdgeEffectFrame, edge: .top, edgeSize: previewEdgeEffectFrame.height, transition: previewTransition) + previewTransition.setAlpha(view: self.previewEdgeEffectView, alpha: !self.scrolledUp ? 1.0 : 0.0) var profileLogoContents: [AnyComponentWithIdentity] = [] profileLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( @@ -1302,7 +1276,8 @@ final class UserAppearanceScreenComponent: Component { return (TelegramTextAttributes.URL, contents) } ) - let previewFooterText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.ProfileColorSetup_ProfileColorPreviewInfo, attributes: footerAttributes)) + let previewFooterRawText = environment.strings.ProfileColorSetup_ProfileColorPreviewInfo.replacingOccurrences(of: " >]", with: "\u{00A0}>]") + let previewFooterText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(previewFooterRawText, attributes: footerAttributes)) if let range = previewFooterText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { previewFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: previewFooterText.string)) } @@ -1335,8 +1310,11 @@ final class UserAppearanceScreenComponent: Component { self.updatedPeerProfileEmoji = nil self.updatedPeerStatus = nil } - self.currentSection = .name - self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true))) + if let controller = self.environment?.controller() as? UserAppearanceScreen { + controller.switchToSection(.name) + } else { + self.switchToSection(.name) + } } )), items: [ @@ -1655,8 +1633,10 @@ final class UserAppearanceScreenComponent: Component { } contentHeight += namePreviewSize.height - 38.0 - transition.setFrame(view: self.previewShadowView, frame: namePreviewFrame.insetBy(dx: -45.0, dy: -45.0)) - previewTransition.setAlpha(view: self.previewShadowView, alpha: !self.scrolledUp ? 1.0 : 0.0) + let previewEdgeEffectFrame = CGRect(origin: CGPoint(x: namePreviewFrame.minX, y: namePreviewFrame.maxY - UIScreenPixel), size: CGSize(width: namePreviewFrame.width, height: previewEdgeEffectHeight)) + previewTransition.setFrame(view: self.previewEdgeEffectView, frame: previewEdgeEffectFrame) + self.previewEdgeEffectView.update(content: environment.theme.list.itemBlocksBackgroundColor, alpha: 1.0, rect: previewEdgeEffectFrame, edge: .top, edgeSize: previewEdgeEffectFrame.height, transition: previewTransition) + previewTransition.setAlpha(view: self.previewEdgeEffectView, alpha: !self.scrolledUp ? 1.0 : 0.0) let nameColorSectionSize = self.nameColorSection.update( transition: transition, @@ -1997,11 +1977,20 @@ final class UserAppearanceScreenComponent: Component { public class UserAppearanceScreen: ViewControllerComponentContainer { private let context: AccountContext fileprivate let focusOnItemTag: UserAppearanceEntryTag? - - private let overNavigationContainer: UIView + fileprivate var selectedSection: UserAppearanceScreenComponent.View.Section + private let segmentedTitleView: ItemListControllerSegmentedTitleView private var didSetReady: Bool = false + private static func initialSection(for focusOnItemTag: UserAppearanceEntryTag?) -> UserAppearanceScreenComponent.View.Section { + switch focusOnItemTag { + case .name, .nameAddIcons, .nameUseGift: + return .name + case .profile, .profileAddIcons, .profileUseGift, nil: + return .profile + } + } + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, @@ -2009,21 +1998,34 @@ public class UserAppearanceScreen: ViewControllerComponentContainer { ) { self.context = context self.focusOnItemTag = focusOnItemTag - - self.overNavigationContainer = SparseContainerView() - + + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + let initialSection = UserAppearanceScreen.initialSection(for: focusOnItemTag) + self.selectedSection = initialSection + self.segmentedTitleView = ItemListControllerSegmentedTitleView( + theme: presentationData.theme, + segments: [ + presentationData.strings.ProfileColorSetup_TitleProfile, + presentationData.strings.ProfileColorSetup_TitleName + ], + selectedIndex: initialSection.segmentIndex + ) + super.init(context: context, component: UserAppearanceScreenComponent( - context: context, - overNavigationContainer: self.overNavigationContainer + context: context ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: updatedPresentationData) - + self.automaticallyControlPresentationContextLayout = false - + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.navigationItem.titleView = self.segmentedTitleView + + self.segmentedTitleView.indexUpdated = { [weak self] index in + self?.switchToSection(UserAppearanceScreenComponent.View.Section(segmentIndex: index)) + } self.ready.set(.never()) @@ -2041,9 +2043,25 @@ public class UserAppearanceScreen: ViewControllerComponentContainer { return componentView.attemptNavigation(complete: complete) } + } + + fileprivate func updateSegmentedTitleView(theme: PresentationTheme, strings: PresentationStrings) { + self.segmentedTitleView.theme = theme + self.segmentedTitleView.segments = [ + strings.ProfileColorSetup_TitleProfile, + strings.ProfileColorSetup_TitleName + ] + self.segmentedTitleView.index = self.selectedSection.segmentIndex + } + + fileprivate func switchToSection(_ section: UserAppearanceScreenComponent.View.Section) { + self.selectedSection = section + self.segmentedTitleView.index = section.segmentIndex - if let navigationBar = self.navigationBar { - navigationBar.customOverBackgroundContentView.insertSubview(self.overNavigationContainer, at: 0) + if let componentView = self.node.hostView.componentView as? UserAppearanceScreenComponent.View { + componentView.switchToSection(section) + } else { + self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3)) } } @@ -2160,32 +2178,3 @@ final class TopBottomCornersComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } - -private func generatePreviewShadowImage() -> UIImage { - let cornerRadius: CGFloat = 26.0 - let shadowInset: CGFloat = 45.0 - - let side = (cornerRadius + 5.0) * 2.0 - let fullSide = shadowInset * 2.0 + side - - return generateImage(CGSize(width: fullSide, height: fullSide), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - let edgeHeight = shadowInset + cornerRadius + 11.0 - context.clip(to: CGRect(x: shadowInset, y: size.height - edgeHeight, width: side, height: edgeHeight)) - - let rect = CGRect(origin: .zero, size: CGSize(width: fullSide, height: fullSide)).insetBy(dx: shadowInset + 1.0, dy: shadowInset + 2.0) - let path = CGPath(roundedRect: rect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) - - let drawShadow = { - context.addPath(path) - context.setShadow(offset: CGSize(), blur: 80.0, color: UIColor.black.cgColor) - context.setFillColor(UIColor.black.cgColor) - context.fillPath() - } - - drawShadow() - drawShadow() - drawShadow() - })!.stretchableImage(withLeftCapWidth: Int(shadowInset + cornerRadius + 5), topCapHeight: Int(shadowInset + cornerRadius + 5)).withRenderingMode(.alwaysTemplate) -} diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift index c9e6afe3e3..c4247157e5 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift @@ -331,7 +331,7 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)) } - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + 1.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame var animationContent: EmojiStatusComponent.AnimationContent? diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift index b4a5c4fe28..7b7523dfee 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ReactionChatPreviewItem.swift @@ -320,7 +320,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode { attributes.append(ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [MessageReaction(value: reaction, count: 1, chosenOrder: 0)], recentPeers: recentPeers, topPeers: [])) } - let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [EngineRawMessage(stableId: 1, stableVersion: 0, id: EngineMessage.Id(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true, isPreview: true, isStandalone: false, rank: nil, rankRole: nil) + let messageItem = item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [EngineRawMessage(stableId: 1, stableVersion: 0, id: EngineMessage.Id(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[userPeerId], text: messageText, attributes: attributes, media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])], theme: item.theme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: item.availableReactions, accountPeer: item.accountPeer, isCentered: true, isPreview: item.reaction == nil, isStandalone: false, rank: nil, rankRole: nil) var node: ListViewItemNode? if let current = currentNode { diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD index 49b5ed0d22..98a1be1ec5 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", "//submodules/AccountContext", + "//submodules/ItemListUI", "//submodules/PresentationDataUtils", "//submodules/WallpaperBackgroundNode", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift index 3b656ee5be..854e812593 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift @@ -7,6 +7,7 @@ import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext +import ItemListUI import PresentationDataUtils import MediaResources import WallpaperGalleryScreen @@ -57,7 +58,7 @@ public final class ThemeAccentColorController: ViewController { return self._ready } - private let segmentedTitleView: ThemeColorSegmentedTitleView + private let segmentedTitleView: ItemListControllerSegmentedTitleView private var applyDisposable = MetaDisposable() @@ -72,7 +73,11 @@ public final class ThemeAccentColorController: ViewController { let section: ThemeColorSection = .background self.section = section - self.segmentedTitleView = ThemeColorSegmentedTitleView(theme: self.presentationData.theme, strings: self.presentationData.strings, selectedSection: section) + self.segmentedTitleView = ItemListControllerSegmentedTitleView(theme: self.presentationData.theme, segments: [ + self.presentationData.strings.Theme_Colors_Background, + self.presentationData.strings.Theme_Colors_Accent, + self.presentationData.strings.Theme_Colors_Messages + ], selectedIndex: section.rawValue) if case .background = mode { self.initialBackgroundColor = randomBackgroundColors.randomElement().flatMap { UIColor(rgb: UInt32(bitPattern: $0)) } @@ -80,38 +85,43 @@ public final class ThemeAccentColorController: ViewController { self.initialBackgroundColor = nil } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings)) + super.init(navigationBarPresentationData: nil) //NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) self.navigationPresentation = .modal + self._hasGlassStyle = true self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.segmentedTitleView.sectionUpdated = { [weak self] section in - if let strongSelf = self { - strongSelf.controllerNode.updateSection(section) - } - } - - self.segmentedTitleView.shouldUpdateSection = { [weak self] section, f in - guard let strongSelf = self else { - f(false) + self.segmentedTitleView.indexUpdated = { [weak self] index in + guard let strongSelf = self, let section = ThemeColorSection(rawValue: index) else { return } + if strongSelf.segmentedTitleView.index == index { + return + } + + let updateSection: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.segmentedTitleView.index = index + strongSelf.controllerNode.updateSection(section) + } + guard section == .background else { - f(true) + updateSection() return } if strongSelf.controllerNode.requiresWallpaperChange { let controller = textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Theme_Colors_ColorWallpaperWarning, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { - f(false) }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Theme_Colors_ColorWallpaperWarningProceed, action: { - f(true) + updateSection() })]) strongSelf.present(controller, in: .window(.root)) } else { - f(true) + updateSection() } } @@ -119,8 +129,7 @@ public final class ThemeAccentColorController: ViewController { self.title = self.presentationData.strings.Wallpaper_Title self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } else { - self.navigationItem.titleView = self.segmentedTitleView - self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) + //self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) } } @@ -373,13 +382,12 @@ public final class ThemeAccentColorController: ViewController { }, ready: self._ready) self.controllerNode.themeUpdated = { [weak self] theme in if let strongSelf = self { - strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: theme, presentationStrings: strongSelf.presentationData.strings), transition: .immediate) strongSelf.segmentedTitleView.theme = theme } } self.controllerNode.requestSectionUpdate = { [weak self] section in if let strongSelf = self { - strongSelf.segmentedTitleView.setIndex(section.rawValue, animated: true) + strongSelf.segmentedTitleView.index = section.rawValue } } @@ -634,6 +642,25 @@ public final class ThemeAccentColorController: ViewController { public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) + let navigationLayout = self.navigationLayout(layout: layout) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout.navigationFrame.maxY, transition: transition) + + if case .background = self.mode { + if self.segmentedTitleView.superview != nil { + self.segmentedTitleView.removeFromSuperview() + } + } else { + if self.segmentedTitleView.superview == nil { + self.view.addSubview(self.segmentedTitleView) + } + + let segmentedTitleFrame = CGRect( + x: layout.safeInsets.left, + y: navigationLayout.navigationFrame.maxY - navigationLayout.defaultContentHeight, + width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, + height: navigationLayout.defaultContentHeight + ) + transition.updateFrame(view: self.segmentedTitleView, frame: segmentedTitleFrame) + } } } diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 198d9b12b1..6a0523e9bd 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -360,6 +360,9 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate self.backgroundContainerNode.addSubnode(self.backgroundWrapperNode) self.backgroundWrapperNode.addSubnode(self.backgroundNode) + self.patternButtonNode.dark = true + self.colorsButtonNode.dark = true + self.patternButtonNode.addTarget(self, action: #selector(self.togglePattern), forControlEvents: .touchUpInside) self.colorsButtonNode.addTarget(self, action: #selector(self.toggleColors), forControlEvents: .touchUpInside) self.playButtonNode.addTarget(self, action: #selector(self.playPressed), forControlEvents: .touchUpInside) @@ -601,8 +604,6 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate if let strongSelf = self { strongSelf.patternPanelNode.serviceBackgroundColor = color strongSelf.pageControlBackgroundNode.backgroundColor = color - strongSelf.patternButtonNode.buttonColor = color - strongSelf.colorsButtonNode.buttonColor = color strongSelf.playButtonBackgroundNode.updateColor(color: color, transition: .immediate) } }) @@ -625,6 +626,8 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.isPagingEnabled = true self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false + self.pageControlNode.setPage(0.0) self.colorPanelNode.view.disablesInteractiveTransitionGestureRecognizer = true self.patternPanelNode.view.disablesInteractiveTransitionGestureRecognizer = true @@ -867,6 +870,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in + }, performBotConnectionReviewAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openStories: { _, _ in }, openStarsTopup: { _ in @@ -1146,7 +1150,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate self.messageNodes = messageNodes } - let toolbarHeight = 49.0 + layout.intrinsicInsets.bottom + let toolbarHeight = 52.0 + layout.intrinsicInsets.bottom var relativeOffset: CGFloat = 0.0 if !self.state.colorPanelCollapsed && self.state.section == .messages { relativeOffset = (CGFloat(self.state.selectedColor) / CGFloat(max(1, self.state.messagesColors.count))) * (self.colorPanelNode.frame.height + toolbarHeight + 144.0) * -1.0 @@ -1207,13 +1211,13 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate if case let .peer(peer) = self.resultMode, case .user = peer, !self.state.displayPatternPanel { toolbarBottomInset += 58.0 } - let toolbarHeight = 49.0 + toolbarBottomInset + let toolbarHeight = 52.0 + toolbarBottomInset transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight))) - self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: 49.0), layout: layout, transition: transition) + self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: 52.0), layout: layout, transition: transition) var bottomInset = toolbarHeight - let standardInputHeight = layout.deviceMetrics.keyboardHeight(inLandscape: false) - let inputFieldPanelHeight: CGFloat = 47.0 + let standardInputHeight = layout.deviceMetrics.keyboardHeight(inLandscape: false) + 17.0 + let inputFieldPanelHeight = WallpaperColorPanelNode.topControlsHeight(for: layout.size.width) let colorPanelHeight = max(standardInputHeight, layout.inputHeight ?? 0.0) - bottomInset + inputFieldPanelHeight var colorPanelOffset: CGFloat = 0.0 @@ -1249,7 +1253,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate transition.updateBounds(node: self.messagesContainerNode, bounds: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)) transition.updatePosition(node: self.messagesContainerNode, position: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height).center) - let backgroundSize = CGSize(width: bounds.width, height: bounds.height - (colorPanelHeight - colorPanelOffset)) + let backgroundSize = CGSize(width: bounds.width, height: bounds.height - (colorPanelHeight - colorPanelOffset) + 20.0) transition.updateFrame(node: self.backgroundContainerNode, frame: CGRect(origin: CGPoint(), size: backgroundSize)) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: layout.size)) diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeColorSegmentedTitleView.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeColorSegmentedTitleView.swift deleted file mode 100644 index a8d182994d..0000000000 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeColorSegmentedTitleView.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation -import UIKit -import SegmentedControlNode -import TelegramPresentationData - -final class ThemeColorSegmentedTitleView: UIView { - private let segmentedControlNode: SegmentedControlNode - - var theme: PresentationTheme { - didSet { - self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme)) - } - } - - var index: Int { - get { - return self.segmentedControlNode.selectedIndex - } - set { - self.segmentedControlNode.selectedIndex = newValue - } - } - - func setIndex(_ index: Int, animated: Bool) { - self.segmentedControlNode.setSelectedIndex(index, animated: animated) - } - - var sectionUpdated: ((ThemeColorSection) -> Void)? - var shouldUpdateSection: ((ThemeColorSection, @escaping (Bool) -> Void) -> Void)? - - init(theme: PresentationTheme, strings: PresentationStrings, selectedSection: ThemeColorSection) { - self.theme = theme - - let sections = [strings.Theme_Colors_Background, strings.Theme_Colors_Accent, strings.Theme_Colors_Messages] - self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: sections.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedSection.rawValue) - - super.init(frame: CGRect()) - - self.segmentedControlNode.selectedIndexChanged = { [weak self] index in - if let section = ThemeColorSection(rawValue: index) { - self?.sectionUpdated?(section) - } - } - - self.segmentedControlNode.selectedIndexShouldChange = { [weak self] index, f in - if let section = ThemeColorSection(rawValue: index) { - self?.shouldUpdateSection?(section, f) - } else { - f(false) - } - } - - self.addSubnode(self.segmentedControlNode) - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - let size = self.bounds.size - let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: size.width + 20.0), transition: .immediate) - self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floor((size.width - controlSize.width) / 2.0), y: floor((size.height - controlSize.height) / 2.0)), size: controlSize) - } -} diff --git a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift index fe727d90ef..3c2f83417d 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift @@ -659,7 +659,7 @@ public class ThemeSettingsThemeItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD index 45b7b88b4e..9737db2810 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/BUILD @@ -32,6 +32,9 @@ swift_library( "//submodules/LegacyMediaPickerUI", "//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode", "//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/GlassControls", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift index b4f8d02740..35fbca796e 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPanelNode.swift @@ -3,31 +3,25 @@ import UIKit import AsyncDisplayKit import SwiftSignalKit import Display +import ComponentFlow import TelegramPresentationData import HexColor +import GlassBackgroundComponent -private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)? -private func textInputBackgroundImage(fieldColor: UIColor, strokeColor: UIColor, diameter: CGFloat) -> UIImage? { - if let current = currentTextInputBackgroundImage { - if current.0.isEqual(fieldColor) && current.1.isEqual(strokeColor) && current.2.isEqual(to: diameter) { - return current.3 - } +private final class ColorPanelGlassBackgroundNode: ASDisplayNode { + override init() { + super.init() + + self.setViewBlock({ + return GlassBackgroundView() + }) + self.isUserInteractionEnabled = false } - - let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in - context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - context.setFillColor(fieldColor.cgColor) - context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - context.setStrokeColor(strokeColor.cgColor) - let strokeWidth: CGFloat = 1.0 - context.setLineWidth(strokeWidth) - context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) - })?.stretchableImage(withLeftCapWidth: Int(diameter) / 2, topCapHeight: Int(diameter) / 2) - if let image = image { - currentTextInputBackgroundImage = (fieldColor, strokeColor, diameter, image) - return image - } else { - return nil + + func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, isInteractive: Bool = false, transition: ContainedViewLayoutTransition) { + if let view = self.view as? GlassBackgroundView { + view.update(size: size, cornerRadius: cornerRadius, isDark: isDark, tintColor: .init(kind: .panel), isInteractive: isInteractive, transition: ComponentTransition(transition)) + } } } @@ -37,22 +31,23 @@ private func generateSwatchBorderImage(theme: PresentationTheme) -> UIImage? { private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { private var theme: PresentationTheme - + private let swatchNode: ASDisplayNode + private let swatchBackgroundNode: ColorPanelGlassBackgroundNode private let borderNode: ASImageNode private let removeButton: HighlightableButtonNode - private let textBackgroundNode: ASImageNode + private let textBackgroundNode: ColorPanelGlassBackgroundNode private let selectionNode: ASDisplayNode let textFieldNode: TextFieldNode private let measureNode: ImmediateTextNode private let prefixNode: ASTextNode - + private var gestureRecognizer: UITapGestureRecognizer? - + var colorChanged: ((UIColor, Bool) -> Void)? var colorRemoved: (() -> Void)? var colorSelected: (() -> Void)? - + private var color: UIColor? private var isDefault = false { @@ -60,69 +55,70 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.updateSelectionVisibility() } } - + var isRemovable: Bool = false { didSet { self.removeButton.isUserInteractionEnabled = self.isRemovable } } - + private var previousIsDefault: Bool? private var previousColor: UIColor? private var validLayout: (CGSize, Bool)? - + private var skipEndEditing = false private let displaySwatch: Bool - + init(theme: PresentationTheme, displaySwatch: Bool = true) { self.theme = theme self.displaySwatch = displaySwatch - - self.textBackgroundNode = ASImageNode() - self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: theme.chat.inputPanel.inputBackgroundColor, strokeColor: theme.chat.inputPanel.inputStrokeColor, diameter: 33.0) - self.textBackgroundNode.displayWithoutProcessing = true - self.textBackgroundNode.displaysAsynchronously = false - + + self.textBackgroundNode = ColorPanelGlassBackgroundNode() + self.selectionNode = ASDisplayNode() self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2) self.selectionNode.cornerRadius = 3.0 self.selectionNode.isUserInteractionEnabled = false - + self.textFieldNode = TextFieldNode() self.measureNode = ImmediateTextNode() - + self.prefixNode = ASTextNode() self.prefixNode.attributedText = NSAttributedString(string: "#", font: Font.regular(17.0), textColor: self.theme.chat.inputPanel.inputTextColor) - + self.swatchNode = ASDisplayNode() self.swatchNode.cornerRadius = 10.5 - + self.swatchNode.clipsToBounds = true + + self.swatchBackgroundNode = ColorPanelGlassBackgroundNode() + self.borderNode = ASImageNode() self.borderNode.displaysAsynchronously = false self.borderNode.displayWithoutProcessing = true self.borderNode.image = generateSwatchBorderImage(theme: theme) - + self.removeButton = HighlightableButtonNode() self.removeButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRemoveIcon"), color: theme.chat.inputPanel.inputControlColor), for: .normal) - + super.init() - + self.addSubnode(self.textBackgroundNode) self.addSubnode(self.selectionNode) self.addSubnode(self.textFieldNode) self.addSubnode(self.prefixNode) + self.addSubnode(self.swatchBackgroundNode) self.addSubnode(self.swatchNode) self.addSubnode(self.borderNode) self.addSubnode(self.removeButton) - + self.removeButton.addTarget(self, action: #selector(self.removePressed), forControlEvents: .touchUpInside) } - + override func didLoad() { super.didLoad() - + self.textFieldNode.textField.font = Font.regular(17.0) self.textFieldNode.textField.textColor = self.theme.chat.inputPanel.inputTextColor self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance @@ -134,26 +130,24 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged) self.textFieldNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor - + let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapped(_:))) self.view.addGestureRecognizer(gestureRecognizer) self.gestureRecognizer = gestureRecognizer } - + func updateTheme(_ theme: PresentationTheme) { self.theme = theme - - self.textBackgroundNode.image = textInputBackgroundImage(fieldColor: self.theme.chat.inputPanel.inputBackgroundColor, strokeColor: self.theme.chat.inputPanel.inputStrokeColor, diameter: 33.0) - + self.textFieldNode.textField.textColor = self.isDefault ? self.theme.chat.inputPanel.inputPlaceholderColor : self.theme.chat.inputPanel.inputTextColor self.textFieldNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance self.textFieldNode.textField.tintColor = self.theme.list.itemAccentColor - + self.selectionNode.backgroundColor = theme.chat.inputPanel.panelControlAccentColor.withAlphaComponent(0.2) self.borderNode.image = generateSwatchBorderImage(theme: theme) self.updateBorderVisibility() } - + func setColor(_ color: UIColor, isDefault: Bool = false, update: Bool = true, ended: Bool = true) { self.color = color self.isDefault = isDefault @@ -169,7 +163,7 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.swatchNode.backgroundColor = color self.updateBorderVisibility() } - + private func updateBorderVisibility() { guard let color = self.swatchNode.backgroundColor else { return @@ -181,21 +175,21 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.borderNode.alpha = 0.0 } } - + @objc private func removePressed() { if self.textFieldNode.textField.isFirstResponder { self.skipEndEditing = true } - + self.colorRemoved?() } - + @objc private func tapped(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.colorSelected?() } } - + @objc internal func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { var updated = textField.text ?? "" updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string) @@ -205,28 +199,28 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { if updated.count <= 6 && updated.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789abcdefABCDEF").inverted) == nil { textField.text = updated.uppercased() textField.textColor = self.theme.chat.inputPanel.inputTextColor - + if updated.count == 6, let color = UIColor(hexString: updated) { self.setColor(color) } - + if let (size, _) = self.validLayout { self.updateSelectionLayout(size: size, transition: .immediate) } } return false } - + @objc func textFieldTextChanged(_ sender: UITextField) { if let color = self.colorFromCurrentText() { self.setColor(color) } - + if let (size, _) = self.validLayout { self.updateSelectionLayout(size: size, transition: .immediate) } } - + @objc func textFieldShouldReturn(_ textField: UITextField) -> Bool { self.skipEndEditing = true if let color = self.colorFromCurrentText() { @@ -237,7 +231,7 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.textFieldNode.textField.resignFirstResponder() return false } - + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { self.skipEndEditing = false self.previousColor = self.color @@ -247,7 +241,7 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { return true } - + @objc func textFieldDidEndEditing(_ textField: UITextField) { if !self.skipEndEditing { if let color = self.colorFromCurrentText() { @@ -257,13 +251,13 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { } } } - + func setSkipEndEditingIfNeeded() { if self.textFieldNode.textField.isFirstResponder && self.colorFromCurrentText() != nil { self.skipEndEditing = true } } - + private func colorFromCurrentText() -> UIColor? { if let text = self.textFieldNode.textField.text, text.count == 6, let color = UIColor(hexString: text) { return color @@ -271,43 +265,56 @@ private class ColorInputFieldNode: ASDisplayNode, UITextFieldDelegate { return nil } } - + private func updateSelectionLayout(size: CGSize, transition: ContainedViewLayoutTransition) { self.measureNode.attributedText = NSAttributedString(string: self.textFieldNode.textField.text ?? "", font: self.textFieldNode.textField.font) let size = self.measureNode.updateLayout(size) transition.updateFrame(node: self.selectionNode, frame: CGRect(x: self.textFieldNode.frame.minX, y: 6.0, width: max(0.0, size.width), height: 20.0)) } - + private func updateSelectionVisibility() { self.selectionNode.isHidden = true } - + func updateLayout(size: CGSize, condensed: Bool, transition: ContainedViewLayoutTransition) { self.validLayout = (size, condensed) - - let swatchFrame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 21.0, height: 21.0)) + + let isDark = self.theme.overallDarkAppearance + transition.updateFrame(node: self.textBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + self.textBackgroundNode.update(size: size, cornerRadius: size.height * 0.5, isDark: isDark, transition: transition) + + let swatchInset: CGFloat = max(5.0, floor(size.height * 0.16)) + let swatchSize = CGSize(width: size.height - swatchInset * 2.0, height: size.height - swatchInset * 2.0) + let swatchFrame = CGRect(origin: CGPoint(x: swatchInset, y: swatchInset), size: swatchSize) + transition.updateFrame(node: self.swatchBackgroundNode, frame: swatchFrame) + self.swatchBackgroundNode.update(size: swatchFrame.size, cornerRadius: swatchFrame.height * 0.5, isDark: isDark, transition: transition) transition.updateFrame(node: self.swatchNode, frame: swatchFrame) transition.updateFrame(node: self.borderNode, frame: swatchFrame) + self.swatchNode.cornerRadius = swatchFrame.height * 0.5 + self.swatchBackgroundNode.isHidden = !self.displaySwatch self.swatchNode.isHidden = !self.displaySwatch - + self.borderNode.isHidden = !self.displaySwatch + let textPadding: CGFloat if self.displaySwatch { - textPadding = condensed ? 31.0 : 37.0 + textPadding = swatchFrame.maxX + (condensed ? 7.0 : 10.0) } else { - textPadding = 12.0 + textPadding = condensed ? 6.0 : 12.0 } - - transition.updateFrame(node: self.textBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - transition.updateFrame(node: self.textFieldNode, frame: CGRect(x: textPadding + 10.0, y: 1.0, width: size.width - (21.0 + textPadding), height: size.height - 2.0)) - + + self.textFieldNode.textField.font = Font.regular(17.0) + self.prefixNode.attributedText = NSAttributedString(string: "#", font: Font.regular(17.0), textColor: self.theme.chat.inputPanel.inputTextColor) + + let removeSize = CGSize(width: size.height, height: size.height) + let removeOffset: CGFloat = condensed ? 2.0 : 0.0 + transition.updateFrame(node: self.textFieldNode, frame: CGRect(x: textPadding + 10.0, y: 1.0, width: max(1.0, size.width - textPadding - removeSize.width - 14.0), height: size.height - 2.0)) + self.updateSelectionLayout(size: size, transition: transition) - + let prefixSize = self.prefixNode.measure(size) - transition.updateFrame(node: self.prefixNode, frame: CGRect(origin: CGPoint(x: textPadding - UIScreenPixel, y: 6.0), size: prefixSize)) - - let removeSize = CGSize(width: 33.0, height: 33.0) - let removeOffset: CGFloat = condensed ? 3.0 : 0.0 + transition.updateFrame(node: self.prefixNode, frame: CGRect(origin: CGPoint(x: textPadding - UIScreenPixel, y: floor((size.height - prefixSize.height) / 2.0)), size: prefixSize)) + transition.updateFrame(node: self.removeButton, frame: CGRect(origin: CGPoint(x: size.width - removeSize.width + removeOffset, y: 0.0), size: removeSize)) self.removeButton.alpha = self.isRemovable ? 1.0 : 0.0 } @@ -322,7 +329,7 @@ public struct WallpaperColorPanelNodeState: Equatable { public var preview: Bool public var simpleGradientGeneration: Bool public var suggestedNewColor: HSBColor? - + public init(selection: Int? = nil, colors: [HSBColor], maximumNumberOfColors: Int, rotateAvailable: Bool, rotation: Int32, preview: Bool, simpleGradientGeneration: Bool, suggestedNewColor: HSBColor? = nil) { self.selection = selection self.colors = colors @@ -390,16 +397,14 @@ private final class ColorSampleItemNode: ASImageNode { public final class WallpaperColorPanelNode: ASDisplayNode { private var theme: PresentationTheme - + private var state: WallpaperColorPanelNodeState - - private let backgroundNode: NavigationBackgroundNode - private let topSeparatorNode: ASDisplayNode - private let bottomSeparatorNode: ASDisplayNode + private let rotateButton: HighlightableButtonNode private let swapButton: HighlightableButtonNode + private let sampleItemsBackgroundNode: ColorPanelGlassBackgroundNode + private let addButtonBackgroundNode: ColorPanelGlassBackgroundNode private let addButton: HighlightableButtonNode - private let doneButton: HighlightableButtonNode private let colorPickerNode: WallpaperColorPickerNode private var sampleItemNodes: [ColorSampleItemNode] = [] @@ -408,36 +413,32 @@ public final class WallpaperColorPanelNode: ASDisplayNode { public var colorsChanged: (([HSBColor], Int, Bool) -> Void)? public var colorSelected: (() -> Void)? public var rotate: (() -> Void)? - + public var colorAdded: (() -> Void)? public var colorRemoved: (() -> Void)? - + private var validLayout: (CGSize, CGFloat)? + public static func topControlsHeight(for width: CGFloat) -> CGFloat { + return 52.0 + } + public init(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme - - self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor) - - self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor - self.bottomSeparatorNode = ASDisplayNode() - self.bottomSeparatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor - - self.doneButton = HighlightableButtonNode() - self.doneButton.setImage(PresentationResourcesChat.chatInputPanelApplyButtonImage(theme), for: .normal) - + self.colorPickerNode = WallpaperColorPickerNode(strings: strings) - + self.rotateButton = HighlightableButtonNode() self.rotateButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRotateIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) self.swapButton = HighlightableButtonNode() self.swapButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorSwapIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) + self.sampleItemsBackgroundNode = ColorPanelGlassBackgroundNode() + self.addButtonBackgroundNode = ColorPanelGlassBackgroundNode() self.addButton = HighlightableButtonNode() self.addButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorAddIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) self.multiColorFieldNode = ColorInputFieldNode(theme: theme, displaySwatch: false) - + self.state = WallpaperColorPanelNodeState( selection: 0, colors: [], @@ -447,22 +448,20 @@ public final class WallpaperColorPanelNode: ASDisplayNode { preview: false, simpleGradientGeneration: false ) - + super.init() - - self.backgroundColor = .white - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.topSeparatorNode) - self.addSubnode(self.bottomSeparatorNode) + + self.backgroundColor = .clear + + self.addSubnode(self.sampleItemsBackgroundNode) self.addSubnode(self.multiColorFieldNode) - self.addSubnode(self.doneButton) self.addSubnode(self.colorPickerNode) - + self.addSubnode(self.rotateButton) self.addSubnode(self.swapButton) + self.addSubnode(self.addButtonBackgroundNode) self.addSubnode(self.addButton) - + self.rotateButton.addTarget(self, action: #selector(self.rotatePressed), forControlEvents: .touchUpInside) self.swapButton.addTarget(self, action: #selector(self.swapPressed), forControlEvents: .touchUpInside) self.addButton.addTarget(self, action: #selector(self.addPressed), forControlEvents: .touchUpInside) @@ -496,7 +495,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { }, animated: strongSelf.state.colors.count >= 2) } } - + self.colorPickerNode.colorChanged = { [weak self] color in if let strongSelf = self { strongSelf.updateState({ current in @@ -522,22 +521,22 @@ public final class WallpaperColorPanelNode: ASDisplayNode { } } } - + public func updateTheme(_ theme: PresentationTheme) { self.theme = theme - self.backgroundNode.updateColor(color: self.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate) - self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor - self.bottomSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor self.multiColorFieldNode.updateTheme(theme) + self.rotateButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorRotateIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) + self.swapButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorSwapIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) + self.addButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/ThemeColorAddIcon"), color: theme.chat.inputPanel.panelControlColor), for: .normal) } - + public func updateState(_ f: (WallpaperColorPanelNodeState) -> WallpaperColorPanelNodeState, updateLayout: Bool = true, animated: Bool = true) { var updateLayout = updateLayout let previousColors = self.state.colors let previousPreview = self.state.preview let previousSelection = self.state.selection self.state = f(self.state) - + let colorWasRemovable = self.multiColorFieldNode.isRemovable self.multiColorFieldNode.isRemovable = self.state.colors.count > 1 if colorWasRemovable != self.multiColorFieldNode.isRemovable { @@ -549,7 +548,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { self.colorPickerNode.color = self.state.colors[index] } } - + if updateLayout, let (size, bottomInset) = self.validLayout { self.updateLayout(size: size, bottomInset: bottomInset, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) } @@ -570,36 +569,30 @@ public final class WallpaperColorPanelNode: ASDisplayNode { self.colorsChanged?(self.state.colors, self.state.selection ?? 0, !self.state.preview) } } - + public func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { self.validLayout = (size, bottomInset) - - let condensedLayout = size.width < 375.0 - let separatorHeight = UIScreenPixel - let topPanelHeight: CGFloat = 47.0 - transition.updateFrame(node: self.backgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: topPanelHeight)) - self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition) - transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: separatorHeight)) - transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(x: 0.0, y: topPanelHeight, width: size.width, height: separatorHeight)) - - let fieldHeight: CGFloat = 33.0 - let leftInset: CGFloat - let rightInset: CGFloat - if condensedLayout { - leftInset = 6.0 - rightInset = 6.0 - } else { - leftInset = 15.0 - rightInset = 15.0 - } - - let buttonSize = CGSize(width: 26.0, height: 26.0) - let canAddColors = self.state.colors.count < self.state.maximumNumberOfColors - transition.updateFrame(node: self.addButton, frame: CGRect(origin: CGPoint(x: size.width - rightInset - buttonSize.width, y: floor((topPanelHeight - buttonSize.height) / 2.0)), size: buttonSize)) + let condensedLayout = size.width < 375.0 + let topPanelHeight = WallpaperColorPanelNode.topControlsHeight(for: size.width) + let controlsSize: CGFloat = 40.0 + let fieldHeight = controlsSize + let leftInset: CGFloat = 8.0 + let rightInset: CGFloat = leftInset + let sampleItemSpacing: CGFloat = 6.0 + + let buttonSize = CGSize(width: controlsSize, height: controlsSize) + let canAddColors = self.state.colors.count < self.state.maximumNumberOfColors + let addButtonFrame = CGRect(origin: CGPoint(x: size.width - rightInset - buttonSize.width, y: floor((topPanelHeight - buttonSize.height) / 2.0)), size: buttonSize) + + transition.updateFrame(node: self.addButtonBackgroundNode, frame: addButtonFrame) + self.addButtonBackgroundNode.update(size: buttonSize, cornerRadius: buttonSize.height * 0.5, isDark: self.theme.overallDarkAppearance, transition: transition) + transition.updateAlpha(node: self.addButtonBackgroundNode, alpha: canAddColors ? 1.0 : 0.0) + transition.updateSublayerTransformScale(node: self.addButtonBackgroundNode, scale: canAddColors ? 1.0 : 0.1) + transition.updateFrame(node: self.addButton, frame: addButtonFrame) transition.updateAlpha(node: self.addButton, alpha: canAddColors ? 1.0 : 0.0) transition.updateSublayerTransformScale(node: self.addButton, scale: canAddColors ? 1.0 : 0.1) - + func degreesToRadians(_ degrees: CGFloat) -> CGFloat { var degrees = degrees if degrees >= 270.0 { @@ -614,10 +607,12 @@ public final class WallpaperColorPanelNode: ASDisplayNode { self.swapButton.isHidden = true self.multiColorFieldNode.isHidden = false - let sampleItemSize: CGFloat = 32.0 - let sampleItemSpacing: CGFloat = 15.0 + let sampleCount = self.state.colors.count + let reservedAddWidth = canAddColors ? buttonSize.width + sampleItemSpacing : 0.0 + let sampleItemSize: CGFloat = 34.0 + let sampleItemsInset = floor((controlsSize - sampleItemSize) / 2.0) - var nextSampleX = leftInset + var nextSampleX = leftInset + sampleItemsInset for i in 0 ..< self.state.colors.count { var animateIn = false @@ -644,7 +639,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { if i != 0 { nextSampleX += sampleItemSpacing } - itemNode.frame = CGRect(origin: CGPoint(x: nextSampleX, y: (topPanelHeight - sampleItemSize) / 2.0), size: CGSize(width: sampleItemSize, height: sampleItemSize)) + itemNode.frame = CGRect(origin: CGPoint(x: nextSampleX, y: floor((topPanelHeight - sampleItemSize) / 2.0)), size: CGSize(width: sampleItemSize, height: sampleItemSize)) nextSampleX += sampleItemSize itemNode.update(size: itemNode.bounds.size, color: self.state.colors[i].color, isSelected: self.state.selection == i) @@ -665,17 +660,28 @@ public final class WallpaperColorPanelNode: ASDisplayNode { self.sampleItemNodes.removeSubrange(self.state.colors.count ..< self.sampleItemNodes.count) } - let fieldX = nextSampleX + sampleItemSpacing + if sampleCount > 0 { + let samplesBackgroundFrame = CGRect(x: leftInset, y: floor((topPanelHeight - controlsSize) / 2.0), width: max(controlsSize, nextSampleX - leftInset + sampleItemsInset), height: controlsSize) + transition.updateFrame(node: self.sampleItemsBackgroundNode, frame: samplesBackgroundFrame) + self.sampleItemsBackgroundNode.update(size: samplesBackgroundFrame.size, cornerRadius: controlsSize * 0.5, isDark: self.theme.overallDarkAppearance, transition: transition) + transition.updateAlpha(node: self.sampleItemsBackgroundNode, alpha: 1.0) + } else { + transition.updateFrame(node: self.sampleItemsBackgroundNode, frame: CGRect(x: leftInset, y: floor((topPanelHeight - controlsSize) / 2.0), width: controlsSize, height: controlsSize)) + transition.updateAlpha(node: self.sampleItemsBackgroundNode, alpha: 0.0) + } - let fieldFrame = CGRect(x: fieldX, y: (topPanelHeight - fieldHeight) / 2.0, width: size.width - fieldX - leftInset - (canAddColors ? (buttonSize.width + sampleItemSpacing) : 0.0), height: fieldHeight) + let fieldSpacing = sampleItemSpacing + 3.0 + let fieldX = nextSampleX + (sampleCount > 0 ? fieldSpacing : 0.0) + + let fieldFrame = CGRect(x: fieldX, y: floor((topPanelHeight - fieldHeight) / 2.0), width: max(1.0, size.width - fieldX - rightInset - reservedAddWidth), height: fieldHeight) transition.updateFrame(node: self.multiColorFieldNode, frame: fieldFrame) - self.multiColorFieldNode.updateLayout(size: fieldFrame.size, condensed: false, transition: transition) - - let colorPickerSize = CGSize(width: size.width, height: size.height - topPanelHeight - separatorHeight) - transition.updateFrame(node: self.colorPickerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + separatorHeight), size: colorPickerSize)) - self.colorPickerNode.updateLayout(size: colorPickerSize, transition: transition) + self.multiColorFieldNode.updateLayout(size: fieldFrame.size, condensed: condensedLayout, transition: transition) + + let colorPickerSize = CGSize(width: size.width, height: size.height - topPanelHeight) + transition.updateFrame(node: self.colorPickerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: colorPickerSize.width, height: colorPickerSize.height + bottomInset))) + self.colorPickerNode.updateLayout(size: colorPickerSize, bottomInset: bottomInset, transition: transition) } - + @objc private func rotatePressed() { self.rotate?() self.updateState({ current in @@ -688,7 +694,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { return updated }) } - + @objc private func swapPressed() { /*self.updateState({ current in var updated = current @@ -699,7 +705,7 @@ public final class WallpaperColorPanelNode: ASDisplayNode { return updated })*/ } - + @objc private func addPressed() { self.colorSelected?() self.colorAdded?() diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift index ec5198f61f..244b49cb9d 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperColorPickerNode.swift @@ -21,7 +21,7 @@ private let knobBackgroundImage: UIImage? = { }() private let pointerImage: UIImage? = { - return generateImage(CGSize(width: 12.0, height: 55.0), opaque: false, scale: nil, rotatedContext: { size, context in + return generateImage(CGSize(width: 12.0, height: 60.0), opaque: false, scale: nil, rotatedContext: { size, context in context.setBlendMode(.clear) context.setFillColor(UIColor.clear.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) @@ -49,19 +49,6 @@ private let pointerImage: UIImage? = { }) }() -private let brightnessMaskImage: UIImage? = { - return generateImage(CGSize(width: 36.0, height: 36.0), opaque: false, scale: nil, rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - - context.setFillColor(UIColor.white.cgColor) - context.fill(bounds) - - context.setBlendMode(.clear) - context.setFillColor(UIColor.clear.cgColor) - context.fillEllipse(in: bounds) - })?.stretchableImage(withLeftCapWidth: 18, topCapHeight: 18) -}() - private let brightnessGradientImage: UIImage? = { return generateImage(CGSize(width: 160.0, height: 1.0), opaque: false, scale: nil, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -227,7 +214,6 @@ private final class WallpaperColorHueSaturationNode: ASDisplayNode { private final class WallpaperColorBrightnessNode: ASDisplayNode { private let gradientNode: ASImageNode - private let maskNode: ASImageNode var hsb: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) { didSet { @@ -244,25 +230,17 @@ private final class WallpaperColorBrightnessNode: ASDisplayNode { self.gradientNode.displayWithoutProcessing = true self.gradientNode.image = brightnessGradientImage self.gradientNode.contentMode = .scaleToFill - - self.maskNode = ASImageNode() - self.maskNode.displaysAsynchronously = false - self.maskNode.displayWithoutProcessing = true - self.maskNode.image = brightnessMaskImage - self.maskNode.contentMode = .scaleToFill - + super.init() self.isOpaque = true self.addSubnode(self.gradientNode) - self.addSubnode(self.maskNode) } override func layout() { super.layout() self.gradientNode.frame = self.bounds - self.maskNode.frame = self.bounds } } @@ -335,21 +313,23 @@ final class WallpaperColorPickerNode: ASDisplayNode { init(strings: PresentationStrings) { self.brightnessNode = WallpaperColorBrightnessNode() + self.brightnessNode.clipsToBounds = true self.brightnessNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0) self.brightnessKnobNode = ASImageNode() self.brightnessKnobNode.image = pointerImage self.brightnessKnobNode.isUserInteractionEnabled = false self.colorNode = WallpaperColorHueSaturationNode() self.colorNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0) + self.colorNode.clipsToBounds = true self.colorKnobNode = WallpaperColorKnobNode() super.init() - self.backgroundColor = .white + self.backgroundColor = .clear + self.addSubnode(self.colorNode) self.addSubnode(self.brightnessNode) self.addSubnode(self.brightnessKnobNode) - self.addSubnode(self.colorNode) self.addSubnode(self.colorKnobNode) self.update() @@ -432,7 +412,7 @@ final class WallpaperColorPickerNode: ASDisplayNode { } private func update() { - self.backgroundColor = .white + self.backgroundColor = .clear self.colorNode.value = self.color.brightness self.brightnessNode.hsb = self.color.values self.colorKnobNode.color = self.color @@ -452,20 +432,29 @@ final class WallpaperColorPickerNode: ASDisplayNode { colorKnobFrame.origin = origin transition.updateFrame(node: self.colorKnobNode, frame: colorKnobFrame) - let inset: CGFloat = 15.0 - let brightnessKnobSize = CGSize(width: 12.0, height: 55.0) + let inset: CGFloat = 16.0 + let brightnessKnobSize = CGSize(width: 12.0, height: 60.0) let brightnessKnobFrame = CGRect(x: inset - brightnessKnobSize.width / 2.0 + (size.width - inset * 2.0) * (1.0 - self.color.brightness), y: size.height - 65.0, width: brightnessKnobSize.width, height: brightnessKnobSize.height) transition.updateFrame(node: self.brightnessKnobNode, frame: brightnessKnobFrame) } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, bottomInset: CGFloat = 0.0, transition: ContainedViewLayoutTransition) { self.validLayout = size let colorHeight = size.height - 66.0 - transition.updateFrame(node: self.colorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: colorHeight)) + let colorVisualHeight = colorHeight + bottomInset + transition.updateFrame(node: self.colorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: colorVisualHeight)) + self.colorNode.cornerRadius = min(32.0, colorVisualHeight * 0.5) + if #available(iOS 11.0, *) { + self.colorNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } + if #available(iOS 13.0, *) { + self.colorNode.layer.cornerCurve = .continuous + } - let inset: CGFloat = 15.0 - transition.updateFrame(node: self.brightnessNode, frame: CGRect(x: inset, y: size.height - 55.0, width: size.width - inset * 2.0, height: 35.0)) + let inset: CGFloat = 16.0 + transition.updateFrame(node: self.brightnessNode, frame: CGRect(x: inset, y: size.height - 55.0, width: size.width - inset * 2.0, height: 40.0)) + self.brightnessNode.cornerRadius = 20.0 self.updateKnobLayout(size: size, panningColor: false, transition: .immediate) } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift index ca30053e7f..780aab8723 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperCropNode.swift @@ -41,6 +41,7 @@ final class WallpaperCropNode: ASDisplayNode, ASScrollViewDelegate { self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.decelerationRate = UIScrollView.DecelerationRate.fast + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift index 6bd88ad0cd..beacb3441d 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift @@ -169,12 +169,16 @@ class WallpaperGalleryInteraction { let beginTransitionToEditor: () -> Void let beginTransitionFromEditor: (Bool) -> Void let finishTransitionFromEditor: () -> Void + let toolbarCancel: () -> Void + let toolbarDone: (Bool) -> Void - init(editMedia: @escaping (PHAsset, UIImage, CGRect, TGMediaEditAdjustments?, UIView, @escaping (UIImage?, TGMediaEditAdjustments?) -> Void, @escaping (UIImage?) -> Void) -> Void, beginTransitionToEditor: @escaping () -> Void, beginTransitionFromEditor: @escaping (Bool) -> Void, finishTransitionFromEditor: @escaping () -> Void) { + init(editMedia: @escaping (PHAsset, UIImage, CGRect, TGMediaEditAdjustments?, UIView, @escaping (UIImage?, TGMediaEditAdjustments?) -> Void, @escaping (UIImage?) -> Void) -> Void, beginTransitionToEditor: @escaping () -> Void, beginTransitionFromEditor: @escaping (Bool) -> Void, finishTransitionFromEditor: @escaping () -> Void, toolbarCancel: @escaping () -> Void, toolbarDone: @escaping (Bool) -> Void) { self.editMedia = editMedia self.beginTransitionToEditor = beginTransitionToEditor self.beginTransitionFromEditor = beginTransitionFromEditor self.finishTransitionFromEditor = finishTransitionFromEditor + self.toolbarCancel = toolbarCancel + self.toolbarDone = toolbarDone } } @@ -214,14 +218,12 @@ public class WallpaperGalleryController: ViewController { private var previousCentralEntryIndex: Int? private let centralItemSubtitle = Promise() - private let centralItemStatus = Promise() private let centralItemAction = Promise() private let centralItemAttributesDisposable = DisposableSet(); private var validLayout: (ContainerViewLayout, CGFloat)? private var overlayNode: WallpaperGalleryOverlayNode? - private var toolbarNode: WallpaperGalleryToolbarNode? private var patternPanelNode: WallpaperPatternPanelNode? private var colorsPanelNode: WallpaperColorPanelNode? @@ -231,6 +233,8 @@ public class WallpaperGalleryController: ViewController { private var savedPatternWallpaper: TelegramWallpaper? private var savedPatternIntensity: Int32? + private var toolbarDoneIsSolid = false + private var toolbarDoneDismissed = false public var requiredLevel: Int? @@ -243,9 +247,10 @@ public class WallpaperGalleryController: ViewController { super.init(navigationBarPresentationData: nil) self.navigationPresentation = .modal + self._hasGlassStyle = true self.title = self.presentationData.strings.WallpaperPreview_Title - //self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.interaction = WallpaperGalleryInteraction(editMedia: { [weak self] asset, image, cropRect, adjustments, referenceView, apply, fullSizeApply in @@ -266,22 +271,11 @@ public class WallpaperGalleryController: ViewController { self.present(c, in: .window(.root)) } }) - }, beginTransitionToEditor: { [weak self] in - guard let self else { - return - } - let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - if let toolbarNode = self.toolbarNode { - transition.updateAlpha(node: toolbarNode, alpha: 0.0) - } + }, beginTransitionToEditor: { }, beginTransitionFromEditor: { [weak self] saving in guard let self else { return } - let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - if let toolbarNode = self.toolbarNode { - transition.updateAlpha(node: toolbarNode, alpha: 1.0) - } if let centralItemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { centralItemNode.beginTransitionFromEditor(saving: saving) } @@ -292,6 +286,10 @@ public class WallpaperGalleryController: ViewController { if let centralItemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { centralItemNode.finishTransitionFromEditor() } + }, toolbarCancel: { [weak self] in + self?.dismiss(forceAway: true) + }, toolbarDone: { [weak self] forBoth in + self?.toolbarDonePressed(forBoth: forBoth) }) var entries: [WallpaperGalleryEntry] = [] @@ -359,19 +357,6 @@ public class WallpaperGalleryController: ViewController { } })) - self.centralItemAttributesDisposable.add(self.centralItemStatus.get().start(next: { [weak self] status in - if let strongSelf = self { - let enabled: Bool - switch status { - case .Local: - enabled = true - default: - enabled = false - } - strongSelf.toolbarNode?.setDoneEnabled(enabled) - } - })) - self.centralItemAttributesDisposable.add(self.centralItemAction.get().start(next: { [weak self] barButton in if let strongSelf = self { strongSelf.navigationItem.rightBarButtonItem = barButton @@ -395,7 +380,7 @@ public class WallpaperGalleryController: ViewController { } self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData), transition: .immediate) - self.toolbarNode?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.updateVisibleItemToolbars(layout: self.validLayout?.0, transition: .immediate) self.patternPanelNode?.updateTheme(self.presentationData.theme) self.colorsPanelNode?.updateTheme(self.presentationData.theme) @@ -408,6 +393,293 @@ public class WallpaperGalleryController: ViewController { self.presentingViewController?.dismiss(animated: true, completion: nil) //self.galleryNode.modalAnimateOut(completion: completion) } + + private func toolbarHeight() -> CGFloat { + var toolbarHeight: CGFloat = 66.0 + if case let .peer(peer, _) = self.mode, case .user = peer { + toolbarHeight += 58.0 + } + return toolbarHeight + } + + private func toolbarDoneButtonType() -> WallpaperGalleryToolbarDoneButtonType { + var doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set + switch self.source { + case let .wallpaper(wallpaper, _, _, _, _, _): + switch wallpaper { + case let .file(file): + if file.id == 0 { + doneButtonType = .none + } + default: + break + } + default: + break + } + if case let .peer(peer, _) = self.mode { + if case .user = peer { + doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) + } else { + doneButtonType = .setChannel + } + } + return doneButtonType + } + + private func itemArguments(colorPreview: Bool = false, isColorsList: Bool = false, patternEnabled: Bool = false) -> WallpaperGalleryItemArguments { + return WallpaperGalleryItemArguments( + colorPreview: colorPreview, + isColorsList: isColorsList, + patternEnabled: patternEnabled, + toolbarHeight: self.toolbarHeight(), + toolbarDoneButtonType: self.toolbarDoneButtonType(), + toolbarRequiredLevel: self.requiredLevel + ) + } + + private func updateVisibleItemToolbars(layout: ContainerViewLayout?, transition: ContainedViewLayoutTransition) { + guard self.isNodeLoaded else { + return + } + let toolbarHeight = self.toolbarHeight() + self.galleryNode.pager.forEachItemNode { itemNode in + guard let itemNode = itemNode as? WallpaperGalleryItemNode else { + return + } + itemNode.updateToolbarThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + itemNode.setToolbarDoneIsSolid(self.toolbarDoneIsSolid, transition: transition) + if let layout { + itemNode.updateToolbarLayout(layout: layout, toolbarHeight: toolbarHeight, transition: transition) + } + } + } + + private func toolbarDonePressed(forBoth: Bool) { + guard !self.toolbarDoneDismissed else { + return + } + + let strongSelf = self + if forBoth && !strongSelf.context.isPremium { + let context = strongSelf.context + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .wallpapers, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .wallpapers, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + strongSelf.push(controller) + + return + } + + if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { + if centralItemNode.cropNode.scrollNode.view.isDecelerating { + return + } + strongSelf.toolbarDoneDismissed = true + let options = centralItemNode.options + if !strongSelf.entries.isEmpty { + let entry = strongSelf.entries[centralItemNode.index] + let apply = strongSelf.apply + if case .peer = strongSelf.mode { + if case let .wallpaper(wallpaper, _) = entry, options.contains(.blur) { + var resource: TelegramMediaResource? + switch wallpaper { + case let .file(file): + resource = file.file.resource + case let .image(representations, _): + if let largestSize = largestImageRepresentation(representations) { + resource = largestSize.resource + } + default: + break + } + if let resource = resource { + let representation = CachedBlurredWallpaperRepresentation() + var data: Data? + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } + + if let data = data { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) + |> filter({ $0.complete }) + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + apply?(entry, options, nil, nil, centralItemNode.brightness, forBoth) + }) + } + } + } else { + apply?(entry, options, centralItemNode.editedFullSizeImage, centralItemNode.editedCropRect, centralItemNode.brightness, forBoth) + } + return + } + + switch entry { + case let .wallpaper(wallpaper, _): + var resource: TelegramMediaResource? + switch wallpaper { + case let .file(file): + resource = file.file.resource + case let .image(representations, _): + if let largestSize = largestImageRepresentation(representations) { + resource = largestSize.resource + } + default: + break + } + + let completion: (TelegramWallpaper) -> Void = { wallpaper in + let baseSettings = wallpaper.settings + let updatedSettings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), colors: baseSettings?.colors ?? [], intensity: baseSettings?.intensity, rotation: baseSettings?.rotation) + let wallpaper = wallpaper.withUpdatedSettings(updatedSettings) + + let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered + let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in + var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers + let wallpaper = wallpaper.isBasicallyEqual(to: strongSelf.presentationData.theme.chat.defaultWallpaper) ? nil : wallpaper + let themeReference: PresentationThemeReference + if autoNightModeTriggered { + themeReference = current.automaticThemeSwitchSetting.theme + } else { + themeReference = current.theme + } + let accentColor = current.themeSpecificAccentColors[themeReference.index] + if let accentColor = accentColor, accentColor.baseColor == .custom { + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = wallpaper + } else { + themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = nil + themeSpecificChatWallpapers[themeReference.index] = wallpaper + } + return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers) + }) |> deliverOnMainQueue).start(completed: { + strongSelf.dismiss(animated: true) + }) + + switch strongSelf.source { + case .wallpaper, .slug: + let _ = saveWallpaper(account: strongSelf.context.account, wallpaper: wallpaper).start() + default: + break + } + let _ = installWallpaper(account: strongSelf.context.account, wallpaper: wallpaper).start() + let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction in + WallpapersState.update(transaction: transaction, { state in + var state = state + if let index = state.wallpapers.firstIndex(where: { + $0.isBasicallyEqual(to: wallpaper) + }) { + state.wallpapers.remove(at: index) + } + state.wallpapers.insert(wallpaper, at: 0) + return state + }) + }).start() + } + + let applyWallpaper: (TelegramWallpaper) -> Void = { wallpaper in + if options.contains(.blur) { + if let resource = resource { + let representation = CachedBlurredWallpaperRepresentation() + var data: Data? + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } + + if let data = data { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) + |> filter({ $0.complete }) + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + completion(wallpaper) + }) + } + } + } else if case let .file(file) = wallpaper, let resource = resource { + if wallpaper.isPattern, !file.settings.colors.isEmpty, let _ = file.settings.intensity { + var data: Data? + var thumbnailData: Data? + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + data = maybeData + } + + let thumbnailResource = file.file.previewRepresentations.first?.resource + + if let resource = thumbnailResource { + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + thumbnailData = maybeData + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + thumbnailData = maybeData + } + } + + if let data = data { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + if let thumbnailResource = thumbnailResource, let thumbnailData = thumbnailData { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) + } + completion(wallpaper) + } + } else if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.file.resource.id)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: data) + completion(wallpaper) + } + } else { + completion(wallpaper) + } + } + + if case let .image(currentRepresentations, currentSettings) = wallpaper { + let _ = (strongSelf.context.wallpaperUploadManager!.stateSignal() + |> take(1) + |> deliverOnMainQueue).start(next: { status in + switch status { + case let .uploaded(uploadedWallpaper, resultWallpaper): + if case let .image(uploadedRepresentations, _) = uploadedWallpaper, uploadedRepresentations == currentRepresentations { + let updatedWallpaper = resultWallpaper.withUpdatedSettings(currentSettings) + applyWallpaper(updatedWallpaper) + return + } + case let .uploading(uploadedWallpaper, _): + if case let .image(uploadedRepresentations, uploadedSettings) = uploadedWallpaper, uploadedRepresentations == currentRepresentations, uploadedSettings != currentSettings { + let updatedWallpaper = uploadedWallpaper.withUpdatedSettings(currentSettings) + applyWallpaper(updatedWallpaper) + return + } + default: + break + } + applyWallpaper(wallpaper) + }) + } else { + var updatedWallpaper = wallpaper + if var settings = wallpaper.settings { + settings.motion = options.contains(.motion) + updatedWallpaper = updatedWallpaper.withUpdatedSettings(settings) + } + applyWallpaper(updatedWallpaper) + } + default: + break + } + + strongSelf.apply?(entry, options, nil, centralItemNode.cropRect, centralItemNode.brightness, forBoth) + } + } + } private func updateTransaction(entries: [WallpaperGalleryEntry], arguments: WallpaperGalleryItemArguments) -> GalleryPagerTransaction { var i: Int = 0 @@ -479,271 +751,10 @@ public class WallpaperGalleryController: ViewController { break } - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let overlayNode = WallpaperGalleryOverlayNode() self.overlayNode = overlayNode self.galleryNode.overlayNode = overlayNode self.galleryNode.addSubnode(overlayNode) - - var doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set - switch self.source { - case let .wallpaper(wallpaper, _, _, _, _, _): - switch wallpaper { - case let .file(file): - if file.id == 0 { - doneButtonType = .none - } - default: - break - } - default: - break - } - if case let .peer(peer, _) = self.mode { - if case .user = peer { - doneButtonType = .setPeer(peer.compactDisplayTitle, self.context.isPremium) - } else { - doneButtonType = .setChannel - } - } - - let toolbarNode = WallpaperGalleryToolbarNode(theme: presentationData.theme, strings: presentationData.strings, doneButtonType: doneButtonType) - toolbarNode.requiredLevel = self.requiredLevel - switch self.source { - case .asset, .contextResult: - toolbarNode.dark = false - default: - toolbarNode.dark = true - } - self.toolbarNode = toolbarNode - overlayNode.addSubnode(toolbarNode) - - toolbarNode.cancel = { [weak self] in - self?.dismiss(forceAway: true) - } - var dismissed = false - toolbarNode.done = { [weak self] forBoth in - if let strongSelf = self, !dismissed { - if forBoth && !strongSelf.context.isPremium { - let context = strongSelf.context - var replaceImpl: ((ViewController) -> Void)? - let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .wallpapers, forceDark: false, action: { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .wallpapers, forceDark: false, dismissed: nil) - replaceImpl?(controller) - }, dismissed: nil) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - strongSelf.push(controller) - - return - } - - if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { - if centralItemNode.cropNode.scrollNode.view.isDecelerating { - return - } - dismissed = true - let options = centralItemNode.options - if !strongSelf.entries.isEmpty { - let entry = strongSelf.entries[centralItemNode.index] - let apply = strongSelf.apply - if case .peer = strongSelf.mode { - if case let .wallpaper(wallpaper, _) = entry, options.contains(.blur) { - var resource: TelegramMediaResource? - switch wallpaper { - case let .file(file): - resource = file.file.resource - case let .image(representations, _): - if let largestSize = largestImageRepresentation(representations) { - resource = largestSize.resource - } - default: - break - } - if let resource = resource { - let representation = CachedBlurredWallpaperRepresentation() - var data: Data? - if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } - - if let data = data { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) - let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) - |> filter({ $0.complete }) - |> take(1) - |> deliverOnMainQueue).start(next: { _ in - apply?(entry, options, nil, nil, centralItemNode.brightness, forBoth) - }) - } - } - } else { - apply?(entry, options, centralItemNode.editedFullSizeImage, centralItemNode.editedCropRect, centralItemNode.brightness, forBoth) - } - return - } - - switch entry { - case let .wallpaper(wallpaper, _): - var resource: TelegramMediaResource? - switch wallpaper { - case let .file(file): - resource = file.file.resource - case let .image(representations, _): - if let largestSize = largestImageRepresentation(representations) { - resource = largestSize.resource - } - default: - break - } - - let completion: (TelegramWallpaper) -> Void = { wallpaper in - let baseSettings = wallpaper.settings - let updatedSettings = WallpaperSettings(blur: options.contains(.blur), motion: options.contains(.motion), colors: baseSettings?.colors ?? [], intensity: baseSettings?.intensity, rotation: baseSettings?.rotation) - let wallpaper = wallpaper.withUpdatedSettings(updatedSettings) - - let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered - let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in - var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers - let wallpaper = wallpaper.isBasicallyEqual(to: strongSelf.presentationData.theme.chat.defaultWallpaper) ? nil : wallpaper - let themeReference: PresentationThemeReference - if autoNightModeTriggered { - themeReference = current.automaticThemeSwitchSetting.theme - } else { - themeReference = current.theme - } - let accentColor = current.themeSpecificAccentColors[themeReference.index] - if let accentColor = accentColor, accentColor.baseColor == .custom { - themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = wallpaper - } else { - themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = nil - themeSpecificChatWallpapers[themeReference.index] = wallpaper - } - return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers) - }) |> deliverOnMainQueue).start(completed: { - self?.dismiss(animated: true) - }) - - switch strongSelf.source { - case .wallpaper, .slug: - let _ = saveWallpaper(account: strongSelf.context.account, wallpaper: wallpaper).start() - default: - break - } - let _ = installWallpaper(account: strongSelf.context.account, wallpaper: wallpaper).start() - let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction in - WallpapersState.update(transaction: transaction, { state in - var state = state - if let index = state.wallpapers.firstIndex(where: { - $0.isBasicallyEqual(to: wallpaper) - }) { - state.wallpapers.remove(at: index) - } - state.wallpapers.insert(wallpaper, at: 0) - return state - }) - }).start() - } - - let applyWallpaper: (TelegramWallpaper) -> Void = { wallpaper in - if options.contains(.blur) { - if let resource = resource { - let representation = CachedBlurredWallpaperRepresentation() - var data: Data? - if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } - - if let data = data { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) - let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) - |> filter({ $0.complete }) - |> take(1) - |> deliverOnMainQueue).start(next: { _ in - completion(wallpaper) - }) - } - } - } else if case let .file(file) = wallpaper, let resource = resource { - if wallpaper.isPattern, !file.settings.colors.isEmpty, let _ = file.settings.intensity { - var data: Data? - var thumbnailData: Data? - if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - data = maybeData - } - - let thumbnailResource = file.file.previewRepresentations.first?.resource - - if let resource = thumbnailResource { - if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - thumbnailData = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - thumbnailData = maybeData - } - } - - if let data = data { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) - if let thumbnailResource = thumbnailResource, let thumbnailData = thumbnailData { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) - } - completion(wallpaper) - } - } else if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.file.resource.id)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: data) - completion(wallpaper) - } - } else { - completion(wallpaper) - } - } - - if case let .image(currentRepresentations, currentSettings) = wallpaper { - let _ = (strongSelf.context.wallpaperUploadManager!.stateSignal() - |> take(1) - |> deliverOnMainQueue).start(next: { status in - switch status { - case let .uploaded(uploadedWallpaper, resultWallpaper): - if case let .image(uploadedRepresentations, _) = uploadedWallpaper, uploadedRepresentations == currentRepresentations { - let updatedWallpaper = resultWallpaper.withUpdatedSettings(currentSettings) - applyWallpaper(updatedWallpaper) - return - } - case let .uploading(uploadedWallpaper, _): - if case let .image(uploadedRepresentations, uploadedSettings) = uploadedWallpaper, uploadedRepresentations == currentRepresentations, uploadedSettings != currentSettings { - let updatedWallpaper = uploadedWallpaper.withUpdatedSettings(currentSettings) - applyWallpaper(updatedWallpaper) - return - } - default: - break - } - applyWallpaper(wallpaper) - }) - } else { - var updatedWallpaper = wallpaper - if var settings = wallpaper.settings { - settings.motion = options.contains(.motion) - updatedWallpaper = updatedWallpaper.withUpdatedSettings(settings) - } - applyWallpaper(updatedWallpaper) - } - default: - break - } - - strongSelf.apply?(entry, options, nil, centralItemNode.cropRect, centralItemNode.brightness, forBoth) - } - } - } - } } private func currentEntry() -> WallpaperGalleryEntry? { @@ -770,11 +781,11 @@ public class WallpaperGalleryController: ViewController { private func bindCentralItemNode(animated: Bool, updated: Bool) { if let node = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { self.centralItemSubtitle.set(node.subtitle.get()) - self.centralItemStatus.set(node.status.get()) self.centralItemAction.set(node.actionButton.get()) node.action = { [weak self] in self?.actionPressed() } + self.updateVisibleItemToolbars(layout: self.validLayout?.0, transition: .immediate) node.requestPatternPanel = { [weak self] enabled, initialWallpaper in if let strongSelf = self, let (layout, _) = strongSelf.validLayout { strongSelf.colorsPanelEnabled = false @@ -901,7 +912,7 @@ public class WallpaperGalleryController: ViewController { entries[centralEntryIndex] = currentEntry self.entries = entries - self.galleryNode.pager.transaction(self.updateTransaction(entries: entries, arguments: WallpaperGalleryItemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled))) + self.galleryNode.pager.transaction(self.updateTransaction(entries: entries, arguments: self.itemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled))) } private func updateEntries(wallpaper: TelegramWallpaper, preview: Bool = false) { @@ -920,7 +931,7 @@ public class WallpaperGalleryController: ViewController { entries[centralEntryIndex] = currentEntry self.entries = entries - self.galleryNode.pager.transaction(self.updateCurrentEntryTransaction(entry: currentEntry, arguments: WallpaperGalleryItemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled), index: centralEntryIndex)) + self.galleryNode.pager.transaction(self.updateCurrentEntryTransaction(entry: currentEntry, arguments: self.itemArguments(colorPreview: preview, isColorsList: false, patternEnabled: self.patternPanelEnabled), index: centralEntryIndex)) } private func updateEntries(pattern: TelegramWallpaper?, intensity: Int32? = nil, preview: Bool = false) { @@ -955,7 +966,7 @@ public class WallpaperGalleryController: ViewController { } self.entries = updatedEntries - self.galleryNode.pager.transaction(self.updateTransaction(entries: updatedEntries, arguments: WallpaperGalleryItemArguments(colorPreview: preview, isColorsList: true, patternEnabled: self.patternPanelEnabled))) + self.galleryNode.pager.transaction(self.updateTransaction(entries: updatedEntries, arguments: self.itemArguments(colorPreview: preview, isColorsList: true, patternEnabled: self.patternPanelEnabled))) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -963,7 +974,9 @@ public class WallpaperGalleryController: ViewController { super.containerLayoutUpdated(layout, transition: transition) - let panelHeight: CGFloat = 235.0 + let panelHeight: CGFloat = 188.0 + WallpaperColorPanelNode.topControlsHeight(for: layout.size.width) + let toolbarHeight = self.toolbarHeight() + self.toolbarDoneIsSolid = self.patternPanelEnabled || self.colorsPanelEnabled var pagerLayout = layout if self.patternPanelEnabled || self.colorsPanelEnabled { @@ -974,13 +987,7 @@ public class WallpaperGalleryController: ViewController { self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.galleryNode.containerLayoutUpdated(pagerLayout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) self.overlayNode?.frame = self.galleryNode.bounds - - var toolbarHeight: CGFloat = 66.0 - if case let .peer(peer, _) = self.mode, case .user = peer { - toolbarHeight += 58.0 - } - transition.updateFrame(node: self.toolbarNode!, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom))) - self.toolbarNode!.updateLayout(size: CGSize(width: layout.size.width, height: toolbarHeight), layout: layout, transition: transition) + self.updateVisibleItemToolbars(layout: layout, transition: transition) var bottomInset = toolbarHeight + layout.intrinsicInsets.bottom @@ -1025,7 +1032,7 @@ public class WallpaperGalleryController: ViewController { } self.patternPanelNode = patternPanelNode currentPatternPanelNode = patternPanelNode - self.overlayNode?.insertSubnode(patternPanelNode, belowSubnode: self.toolbarNode!) + self.overlayNode?.addSubnode(patternPanelNode) } let currentColorsPanelNode: WallpaperColorPanelNode @@ -1035,7 +1042,7 @@ public class WallpaperGalleryController: ViewController { let colorsPanelNode = WallpaperColorPanelNode(theme: self.presentationData.theme, strings: self.presentationData.strings) self.colorsPanelNode = colorsPanelNode currentColorsPanelNode = colorsPanelNode - self.overlayNode?.insertSubnode(colorsPanelNode, belowSubnode: self.toolbarNode!) + self.overlayNode?.addSubnode(colorsPanelNode) colorsPanelNode.colorsChanged = { [weak self] colors, _, _ in guard let strongSelf = self else { @@ -1055,7 +1062,6 @@ public class WallpaperGalleryController: ViewController { } } - let originalBottomInset = bottomInset var patternPanelFrame = CGRect(x: 0.0, y: layout.size.height, width: layout.size.width, height: panelHeight) if self.patternPanelEnabled { patternPanelFrame.origin = CGPoint(x: 0.0, y: layout.size.height - max((layout.inputHeight ?? 0.0) - panelHeight + 44.0, bottomInset) - panelHeight) @@ -1063,7 +1069,7 @@ public class WallpaperGalleryController: ViewController { } transition.updateFrame(node: currentPatternPanelNode, frame: patternPanelFrame) - currentPatternPanelNode.updateLayout(size: patternPanelFrame.size, bottomInset: originalBottomInset, transition: transition) + currentPatternPanelNode.updateLayout(size: patternPanelFrame.size, bottomInset: 0.0, transition: transition) var colorsPanelFrame = CGRect(x: 0.0, y: layout.size.height, width: layout.size.width, height: panelHeight) if self.colorsPanelEnabled { @@ -1071,11 +1077,9 @@ public class WallpaperGalleryController: ViewController { bottomInset += panelHeight } - transition.updateFrame(node: currentColorsPanelNode, frame: CGRect(origin: colorsPanelFrame.origin, size: CGSize(width: colorsPanelFrame.width, height: colorsPanelFrame.height + originalBottomInset))) - currentColorsPanelNode.updateLayout(size: colorsPanelFrame.size, bottomInset: originalBottomInset, transition: transition) + transition.updateFrame(node: currentColorsPanelNode, frame: colorsPanelFrame) + currentColorsPanelNode.updateLayout(size: colorsPanelFrame.size, bottomInset: 0.0, transition: transition) - self.toolbarNode?.setDoneIsSolid(self.patternPanelEnabled || self.colorsPanelEnabled, transition: transition) - bottomInset += toolbarHeight self.validLayout = (layout, bottomInset) @@ -1085,11 +1089,12 @@ public class WallpaperGalleryController: ViewController { colors = true } - self.galleryNode.pager.replaceItems(zip(0 ..< self.entries.count, self.entries).map({ WallpaperGalleryItem(context: self.context, index: $0, entry: $1, arguments: WallpaperGalleryItemArguments(isColorsList: colors), source: self.source, mode: self.mode, interaction: self.interaction!) }), centralItemIndex: self.centralEntryIndex) + self.galleryNode.pager.replaceItems(zip(0 ..< self.entries.count, self.entries).map({ WallpaperGalleryItem(context: self.context, index: $0, entry: $1, arguments: self.itemArguments(isColorsList: colors), source: self.source, mode: self.mode, interaction: self.interaction!) }), centralItemIndex: self.centralEntryIndex) if let initialOptions = self.initialOptions, let itemNode = self.galleryNode.pager.centralItemNode() as? WallpaperGalleryItemNode { itemNode.options = initialOptions } + self.updateVisibleItemToolbars(layout: layout, transition: .immediate) } } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift index cc1c15d67d..7e36181a6d 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift @@ -20,16 +20,24 @@ import WallpaperBackgroundNode import TextFormat import TooltipUI import TelegramNotices +import ComponentFlow +import GlassControls struct WallpaperGalleryItemArguments { let colorPreview: Bool let isColorsList: Bool let patternEnabled: Bool + let toolbarHeight: CGFloat + let toolbarDoneButtonType: WallpaperGalleryToolbarDoneButtonType + let toolbarRequiredLevel: Int? - init(colorPreview: Bool = false, isColorsList: Bool = false, patternEnabled: Bool = false) { + init(colorPreview: Bool = false, isColorsList: Bool = false, patternEnabled: Bool = false, toolbarHeight: CGFloat = 66.0, toolbarDoneButtonType: WallpaperGalleryToolbarDoneButtonType = .set, toolbarRequiredLevel: Int? = nil) { self.colorPreview = colorPreview self.isColorsList = isColorsList self.patternEnabled = patternEnabled + self.toolbarHeight = toolbarHeight + self.toolbarDoneButtonType = toolbarDoneButtonType + self.toolbarRequiredLevel = toolbarRequiredLevel } } @@ -105,10 +113,11 @@ final class WallpaperGalleryItemNode: GalleryItemNode { private let blurredNode: BlurredImageNode let cropNode: WallpaperCropNode - private let cancelButtonNode: WallpaperNavigationButtonNode - private let shareButtonNode: WallpaperNavigationButtonNode - private let dayNightButtonNode: WallpaperNavigationButtonNode - private let editButtonNode: WallpaperNavigationButtonNode + private let topButtons: ComponentView + private var topCanShare = false + private var topCanSwitchTheme = false + private var topCanEdit = false + private var controlsGlassIsDark = true private let buttonsContainerNode: SparseNode private let blurButtonNode: WallpaperOptionButtonNode @@ -117,6 +126,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { private let colorsButtonNode: WallpaperOptionButtonNode private let playButtonNode: WallpaperNavigationButtonNode private let sliderNode: WallpaperSliderNode + private let toolbarNode: WallpaperGalleryToolbarNode + private var toolbarDoneEnabled = true private let messagesContainerNode: ASDisplayNode private var messageNodes: [ListViewItemNode]? @@ -138,6 +149,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { var requestRotateGradient: ((Int32) -> Void)? private var validLayout: (ContainerViewLayout, CGFloat)? + private var validToolbarLayout: (ContainerViewLayout, CGFloat)? private var validOffset: CGFloat? private var initialWallpaper: TelegramWallpaper? @@ -176,6 +188,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) self.messagesContainerNode.isUserInteractionEnabled = false + self.topButtons = ComponentView() self.buttonsContainerNode = SparseNode() self.blurButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_Blurred, value: .check(false)) self.blurButtonNode.setEnabled(false) @@ -192,16 +205,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { }) self.colorsButtonNode = WallpaperOptionButtonNode(title: self.presentationData.strings.WallpaperPreview_WallpaperColors, value: .colors(false, [.clear])) + self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.presentationData.theme, strings: self.presentationData.strings) - self.cancelButtonNode = WallpaperNavigationButtonNode(content: .text(self.presentationData.strings.Common_Cancel), dark: true) - self.cancelButtonNode.enableSaturation = true - self.shareButtonNode = WallpaperNavigationButtonNode(content: .icon(image: UIImage(bundleImageName: "Chat/Links/Share"), size: CGSize(width: 28.0, height: 28.0)), dark: true) - self.shareButtonNode.enableSaturation = true - self.dayNightButtonNode = WallpaperNavigationButtonNode(content: .dayNight(isNight: self.isDarkAppearance), dark: true) - self.dayNightButtonNode.enableSaturation = true - self.editButtonNode = WallpaperNavigationButtonNode(content: .icon(image: UIImage(bundleImageName: "Settings/WallpaperAdjustments"), size: CGSize(width: 28.0, height: 28.0)), dark: true) - self.editButtonNode.enableSaturation = true - self.playButtonPlayImage = generateImage(CGSize(width: 48.0, height: 48.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.white.cgColor) @@ -264,10 +269,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.buttonsContainerNode.addSubnode(self.colorsButtonNode) self.buttonsContainerNode.addSubnode(self.playButtonNode) self.buttonsContainerNode.addSubnode(self.sliderNode) - self.buttonsContainerNode.addSubnode(self.cancelButtonNode) - self.buttonsContainerNode.addSubnode(self.shareButtonNode) - self.buttonsContainerNode.addSubnode(self.dayNightButtonNode) - self.buttonsContainerNode.addSubnode(self.editButtonNode) + self.buttonsContainerNode.addSubnode(self.toolbarNode) self.imageNode.addSubnode(self.brightnessNode) @@ -276,10 +278,6 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.patternButtonNode.addTarget(self, action: #selector(self.togglePattern), forControlEvents: .touchUpInside) self.colorsButtonNode.addTarget(self, action: #selector(self.toggleColors), forControlEvents: .touchUpInside) self.playButtonNode.addTarget(self, action: #selector(self.togglePlay), forControlEvents: .touchUpInside) - self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - self.shareButtonNode.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside) - self.dayNightButtonNode.addTarget(self, action: #selector(self.dayNightPressed), forControlEvents: .touchUpInside) - self.editButtonNode.addTarget(self, action: #selector(self.editPressed), forControlEvents: .touchUpInside) sliderValueChangedImpl = { [weak self] value in if let self { @@ -459,7 +457,6 @@ final class WallpaperGalleryItemNode: GalleryItemNode { @objc private func dayNightPressed() { self.isDarkAppearance = !self.isDarkAppearance - self.dayNightButtonNode.setIsNight(self.isDarkAppearance) if let layout = self.validLayout?.0 { let offset = CGPoint(x: self.validOffset ?? 0.0, y: 0.0) @@ -594,6 +591,20 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.source = source self.mode = mode self.interaction = interaction + + self.toolbarNode.doneButtonType = arguments.toolbarDoneButtonType + self.toolbarNode.requiredLevel = arguments.toolbarRequiredLevel + self.updateToolbarThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.toolbarNode.cancel = { [weak self] in + if let interaction = self?.interaction { + interaction.toolbarCancel() + } + } + self.toolbarNode.done = { [weak self] forBoth in + if let interaction = self?.interaction { + interaction.toolbarDone(forBoth) + } + } if self.arguments.colorPreview != previousArguments.colorPreview { if self.arguments.colorPreview { @@ -696,16 +707,25 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.playButtonNode.setIcon(self.playButtonRotateImage) } - self.cancelButtonNode.enableSaturation = isColor - self.dayNightButtonNode.enableSaturation = isColor - self.editButtonNode.enableSaturation = isColor - self.shareButtonNode.enableSaturation = isColor self.patternButtonNode.backgroundNode.enableSaturation = isColor self.blurButtonNode.backgroundNode.enableSaturation = isColor self.motionButtonNode.backgroundNode.enableSaturation = isColor self.colorsButtonNode.backgroundNode.enableSaturation = isColor self.playButtonNode.enableSaturation = isColor - + + let controlsGlassIsDark = self.presentationData.theme.overallDarkAppearance //!isColor + self.controlsGlassIsDark = controlsGlassIsDark + self.patternButtonNode.backgroundNode.isDark = controlsGlassIsDark + self.blurButtonNode.backgroundNode.isDark = controlsGlassIsDark + self.motionButtonNode.backgroundNode.isDark = controlsGlassIsDark + self.colorsButtonNode.backgroundNode.isDark = controlsGlassIsDark + self.playButtonNode.dark = controlsGlassIsDark + self.toolbarNode.dark = controlsGlassIsDark + self.blurButtonNode.dark = controlsGlassIsDark + self.motionButtonNode.dark = controlsGlassIsDark + self.patternButtonNode.dark = controlsGlassIsDark + self.colorsButtonNode.dark = controlsGlassIsDark + var canShare = false var canSwitchTheme = false var canEdit = false @@ -948,14 +968,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { canSwitchTheme = true } - if canSwitchTheme { - self.dayNightButtonNode.isHidden = false - self.shareButtonNode.isHidden = true - } else { - self.dayNightButtonNode.isHidden = true - self.shareButtonNode.isHidden = !canShare - } - self.editButtonNode.isHidden = !canEdit + self.topCanSwitchTheme = canSwitchTheme + self.topCanShare = canShare + self.topCanEdit = canEdit if self.cropNode.supernode == nil { self.imageNode.contentMode = .scaleAspectFill @@ -1015,6 +1030,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { strongSelf.blurButtonNode.setEnabled(local) strongSelf.motionButtonNode.setEnabled(local) strongSelf.patternButtonNode.setEnabled(local) + strongSelf.setToolbarDoneEnabled(local) } })) @@ -1028,10 +1044,6 @@ final class WallpaperGalleryItemNode: GalleryItemNode { return } strongSelf.statusNode.backgroundNodeColor = color - strongSelf.patternButtonNode.buttonColor = color - strongSelf.blurButtonNode.buttonColor = color - strongSelf.motionButtonNode.buttonColor = color - strongSelf.colorsButtonNode.buttonColor = color })) } else if self.arguments.patternEnabled != previousArguments.patternEnabled { self.patternButtonNode.isSelected = self.arguments.patternEnabled @@ -1066,6 +1078,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.updateButtonsLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition: .immediate) self.updateMessagesLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition:.immediate) } + if let (layout, toolbarHeight) = self.validToolbarLayout { + self.updateToolbarLayout(layout: layout, toolbarHeight: toolbarHeight, offset: CGPoint(x: offset, y: 0.0), transition: .immediate) + } } func updateDismissTransition(_ value: CGFloat) { @@ -1073,6 +1088,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.updateButtonsLayout(layout: layout, offset: CGPoint(x: 0.0, y: value), transition: .immediate) self.updateMessagesLayout(layout: layout, offset: CGPoint(x: 0.0, y: value), transition: .immediate) } + if let (layout, toolbarHeight) = self.validToolbarLayout { + self.updateToolbarLayout(layout: layout, toolbarHeight: toolbarHeight, offset: CGPoint(x: self.validOffset ?? 0.0, y: value), transition: .immediate) + } } var options: WallpaperPresentationOptions { @@ -1296,6 +1314,34 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } transition.updatePosition(node: self.wrapperNode, position: CGPoint(x: layout.size.width / 2.0 + appliedOffset, y: layout.size.height / 2.0)) } + + func setToolbarDoneEnabled(_ enabled: Bool) { + self.toolbarDoneEnabled = enabled + self.toolbarNode.setDoneEnabled(enabled) + } + + func setToolbarDoneIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) { + self.toolbarNode.setDoneIsSolid(isSolid, transition: transition) + } + + func updateToolbarThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.toolbarNode.updateThemeAndStrings(theme: theme, strings: strings) + self.toolbarNode.setDoneEnabled(self.toolbarDoneEnabled) + } + + func updateToolbarLayout(layout: ContainerViewLayout, toolbarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validToolbarLayout = (layout, toolbarHeight) + self.updateToolbarLayout(layout: layout, toolbarHeight: toolbarHeight, offset: CGPoint(x: self.validOffset ?? 0.0, y: 0.0), transition: transition) + } + + private func updateToolbarLayout(layout: ContainerViewLayout, toolbarHeight: CGFloat, offset: CGPoint, transition: ContainedViewLayoutTransition) { + let toolbarFrame = CGRect( + origin: CGPoint(x: offset.x, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom + offset.y), + size: CGSize(width: layout.size.width, height: toolbarHeight + layout.intrinsicInsets.bottom) + ) + transition.updateFrame(node: self.toolbarNode, frame: toolbarFrame) + self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: toolbarHeight), layout: layout, transition: transition) + } func updateButtonsLayout(layout: ContainerViewLayout, offset: CGPoint, transition: ContainedViewLayoutTransition) { let patternButtonSize = self.patternButtonNode.measure(layout.size) @@ -1328,10 +1374,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let buttonSpacing: CGFloat = 18.0 - var toolbarHeight: CGFloat = 66.0 - if let mode = self.mode, case let .peer(peer, _) = mode, case .user = peer { - toolbarHeight += 58.0 - } + let toolbarHeight = self.arguments.toolbarHeight let leftButtonFrame = CGRect(origin: CGPoint(x: floor(layout.size.width / 2.0 - buttonSize.width - buttonSpacing) + offset.x, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom - 54.0 + offset.y + additionalYOffset), size: buttonSize) let centerButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonSize.width) / 2.0) + offset.x, y: layout.size.height - toolbarHeight - layout.intrinsicInsets.bottom - 54.0 + offset.y + additionalYOffset), size: buttonSize) @@ -1362,11 +1405,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } else { sliderFrame = sliderFrame.offsetBy(dx: 0.0, dy: 22.0) } - - let cancelSize = self.cancelButtonNode.measure(layout.size) - let cancelFrame = CGRect(origin: CGPoint(x: 16.0 + offset.x, y: 16.0), size: cancelSize) - let shareFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - 28.0 + offset.x, y: 16.0), size: CGSize(width: 28.0, height: 28.0)) - let editFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - 28.0 + offset.x - 46.0, y: 16.0), size: CGSize(width: 28.0, height: 28.0)) + + var topCanShare = self.topCanShare let centerOffset: CGFloat = 32.0 @@ -1460,7 +1500,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { colorsAlpha = 0.0 blurAlpha = 0.0 playAlpha = 0.0 - self.shareButtonNode.isHidden = true + topCanShare = false } blurFrame = centerButtonFrame } @@ -1486,10 +1526,70 @@ final class WallpaperGalleryItemNode: GalleryItemNode { transition.updateTransformScale(node: self.sliderNode, scale: sliderScale) self.sliderNode.updateLayout(size: sliderFrame.size) - transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame) - transition.updateFrame(node: self.shareButtonNode, frame: shareFrame) - transition.updateFrame(node: self.dayNightButtonNode, frame: shareFrame) - transition.updateFrame(node: self.editButtonNode, frame: editFrame) + let leftControlItems: [GlassControlGroupComponent.Item] = [ + GlassControlGroupComponent.Item( + id: AnyHashable("close"), + content: .icon("Navigation/Close"), + action: { [weak self] in + self?.cancelPressed() + } + ) + ] + var rightControlItems: [GlassControlGroupComponent.Item] = [] + if self.topCanEdit { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("edit"), + content: .icon("Settings/WallpaperAdjustments"), + action: { [weak self] in + self?.editPressed() + } + )) + } + if self.topCanSwitchTheme { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("dayNight"), + content: .animation(self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun"), + action: { [weak self] in + self?.dayNightPressed() + } + )) + } else if topCanShare { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("share"), + content: .icon("Navigation/Share"), + action: { [weak self] in + self?.actionPressed() + } + )) + } + + let topButtonSideInset: CGFloat = 16.0 + let topButtonsSize = self.topButtons.update( + transition: ComponentTransition(transition), + component: AnyComponent(GlassControlPanelComponent( + theme: self.controlsGlassIsDark ? defaultDarkPresentationTheme : self.presentationData.theme, + leftItem: GlassControlPanelComponent.Item( + items: leftControlItems, + background: .panel + ), + centralItem: nil, + rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: rightControlItems, + background: .panel + ), + centerAlignmentIfPossible: true, + isDark: self.controlsGlassIsDark + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - topButtonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: 44.0) + ) + let topButtonsFrame = CGRect(origin: CGPoint(x: topButtonSideInset + layout.safeInsets.left + offset.x, y: topButtonSideInset), size: topButtonsSize) + if let topButtonsView = self.topButtons.view { + if topButtonsView.superview == nil { + self.buttonsContainerNode.view.addSubview(topButtonsView) + } + transition.updateFrame(view: topButtonsView, frame: topButtonsFrame) + } } private func updateMessagesLayout(layout: ContainerViewLayout, offset: CGPoint, transition: ContainedViewLayoutTransition) { @@ -1750,6 +1850,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { self.updateButtonsLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition: transition) self.updateMessagesLayout(layout: layout, offset: CGPoint(x: offset, y: 0.0), transition: transition) + self.updateToolbarLayout(layout: layout, toolbarHeight: self.arguments.toolbarHeight, transition: transition) self.validLayout = (layout, navigationBarHeight) } @@ -1768,7 +1869,11 @@ final class WallpaperGalleryItemNode: GalleryItemNode { return } - let frame = self.dayNightButtonNode.view.convert(self.dayNightButtonNode.bounds, to: self.view) + guard let controlsView = self.topButtons.view as? GlassControlPanelComponent.View, let dayNightView = controlsView.rightItemView?.itemView(id: AnyHashable("dayNight")) else { + return + } + + let frame = dayNightView.convert(dayNightView.bounds, to: self.view) let currentTimestamp = Int32(Date().timeIntervalSince1970) let isDark = self.isDarkAppearance diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift index 0ce64b7b9c..2c1575bd40 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryToolbarNode.swift @@ -6,6 +6,7 @@ import TelegramPresentationData import ManagedAnimationNode import ComponentFlow import PremiumLockButtonSubtitleComponent +import ButtonComponent public enum WallpaperGalleryToolbarCancelButtonType { case cancel @@ -24,169 +25,129 @@ public enum WallpaperGalleryToolbarDoneButtonType { public protocol WallpaperGalleryToolbar: ASDisplayNode { var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { get set } var doneButtonType: WallpaperGalleryToolbarDoneButtonType { get set } - + var cancel: (() -> Void)? { get set } var done: ((Bool) -> Void)? { get set } - + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) - + func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) } public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar { class ButtonNode: ASDisplayNode { private let strings: PresentationStrings - + private let doneButton = HighlightTrackingButtonNode() - private var doneButtonBackgroundNode: ASDisplayNode + private var doneButtonBackgroundNode: WallpaperOptionBackgroundNode private let doneButtonTitleNode: ImmediateTextNode private var doneButtonSubtitle: ComponentView? - + private let doneButtonSolidBackgroundNode: ASDisplayNode private let doneButtonSolidTitleNode: ImmediateTextNode - + private let animationNode: SimpleAnimationNode - + var action: () -> Void = {} - + var isLocked: Bool = false { didSet { self.animationNode.isHidden = !self.isLocked } } - + var requiredLevel: Int? - + init(strings: PresentationStrings) { self.strings = strings - - self.doneButtonBackgroundNode = WallpaperLightButtonBackgroundNode() - self.doneButtonBackgroundNode.cornerRadius = 14.0 - + + self.doneButtonBackgroundNode = WallpaperOptionBackgroundNode(isDark: false) + self.doneButtonTitleNode = ImmediateTextNode() self.doneButtonTitleNode.displaysAsynchronously = false self.doneButtonTitleNode.isUserInteractionEnabled = false - + self.doneButtonSolidBackgroundNode = ASDisplayNode() self.doneButtonSolidBackgroundNode.alpha = 0.0 self.doneButtonSolidBackgroundNode.clipsToBounds = true - self.doneButtonSolidBackgroundNode.layer.cornerRadius = 14.0 + self.doneButtonSolidBackgroundNode.layer.cornerRadius = 26.0 if #available(iOS 13.0, *) { self.doneButtonSolidBackgroundNode.layer.cornerCurve = .continuous } self.doneButtonSolidBackgroundNode.isUserInteractionEnabled = false - + self.doneButtonSolidTitleNode = ImmediateTextNode() self.doneButtonSolidTitleNode.alpha = 0.0 self.doneButtonSolidTitleNode.displaysAsynchronously = false self.doneButtonSolidTitleNode.isUserInteractionEnabled = false - + self.animationNode = SimpleAnimationNode(animationName: "premium_unlock", size: CGSize(width: 30.0, height: 30.0)) self.animationNode.customColor = .white self.animationNode.isHidden = true - + super.init() - + self.doneButton.isExclusiveTouch = true self.addSubnode(self.doneButtonBackgroundNode) - self.addSubnode(self.doneButtonTitleNode) - - self.addSubnode(self.doneButtonSolidBackgroundNode) - self.addSubnode(self.doneButtonSolidTitleNode) - + self.doneButtonBackgroundNode.contentView.addSubnode(self.doneButtonTitleNode) + + self.doneButtonBackgroundNode.contentView.addSubnode(self.doneButtonSolidBackgroundNode) + self.doneButtonBackgroundNode.contentView.addSubnode(self.doneButtonSolidTitleNode) + self.addSubnode(self.animationNode) - - self.addSubnode(self.doneButton) - - self.doneButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - if strongSelf.isSolid { - strongSelf.doneButtonSolidBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonSolidBackgroundNode.alpha = 0.55 - strongSelf.doneButtonSolidTitleNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonSolidTitleNode.alpha = 0.55 - } else { - strongSelf.doneButtonBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonBackgroundNode.alpha = 0.55 - strongSelf.doneButtonTitleNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonTitleNode.alpha = 0.55 - - strongSelf.doneButtonSubtitle?.view?.layer.removeAnimation(forKey: "opacity") - strongSelf.doneButtonSubtitle?.view?.alpha = 0.55 - } - } else { - if strongSelf.isSolid { - strongSelf.doneButtonSolidBackgroundNode.alpha = 1.0 - strongSelf.doneButtonSolidBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - strongSelf.doneButtonSolidTitleNode.alpha = 1.0 - strongSelf.doneButtonSolidTitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - } else { - strongSelf.doneButtonBackgroundNode.alpha = 1.0 - strongSelf.doneButtonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - strongSelf.doneButtonTitleNode.alpha = 1.0 - strongSelf.doneButtonTitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - - strongSelf.doneButtonSubtitle?.view?.alpha = 1.0 - strongSelf.doneButtonSubtitle?.view?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2) - } - } - } - } - + + self.doneButtonBackgroundNode.contentView.addSubview(self.doneButton.view) + self.doneButton.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) } - + func setEnabled(_ enabled: Bool) { self.doneButton.alpha = enabled ? 1.0 : 0.4 self.doneButton.isUserInteractionEnabled = enabled } - + private var isSolid = false func setIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) { guard self.isSolid != isSolid else { return } self.isSolid = isSolid - + transition.updateAlpha(node: self.doneButtonBackgroundNode, alpha: isSolid ? 0.0 : 1.0) transition.updateAlpha(node: self.doneButtonSolidBackgroundNode, alpha: isSolid ? 1.0 : 0.0) transition.updateAlpha(node: self.doneButtonTitleNode, alpha: isSolid ? 0.0 : 1.0) transition.updateAlpha(node: self.doneButtonSolidTitleNode, alpha: isSolid ? 1.0 : 0.0) } - + func updateTitle(_ title: String, theme: PresentationTheme) { - self.doneButtonTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: .white) - + self.doneButtonTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.dark ? .white : .black) + self.doneButtonSolidBackgroundNode.backgroundColor = theme.list.itemCheckColors.fillColor self.doneButtonSolidTitleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor) } - + func updateSize(_ size: CGSize) { let bounds = CGRect(origin: .zero, size: size) self.doneButtonBackgroundNode.frame = bounds - if let backgroundNode = self.doneButtonBackgroundNode as? WallpaperOptionBackgroundNode { - backgroundNode.updateLayout(size: size) - } else if let backgroundNode = self.doneButtonBackgroundNode as? WallpaperLightButtonBackgroundNode { - backgroundNode.updateLayout(size: size) - } + self.doneButtonBackgroundNode.updateLayout(size: size) self.doneButtonSolidBackgroundNode.frame = bounds - + self.doneButtonSolidBackgroundNode.layer.cornerRadius = size.height * 0.5 + let constrainedSize = CGSize(width: size.width - 44.0, height: size.height) let iconSize = CGSize(width: 30.0, height: 30.0) let doneTitleSize = self.doneButtonTitleNode.updateLayout(constrainedSize) - + var totalWidth = doneTitleSize.width if self.isLocked { totalWidth += iconSize.width + 1.0 } let titleOriginX = floorToScreenPixels((bounds.width - totalWidth) / 2.0) - + self.animationNode.frame = CGRect(origin: CGPoint(x: titleOriginX, y: floorToScreenPixels((bounds.height - iconSize.height) / 2.0)), size: iconSize) - + var titleFrame = CGRect(origin: CGPoint(x: titleOriginX + totalWidth - doneTitleSize.width, y: floorToScreenPixels((bounds.height - doneTitleSize.height) / 2.0)), size: doneTitleSize).offsetBy(dx: bounds.minX, dy: bounds.minY) - + if let requiredLevel = self.requiredLevel { let subtitle: ComponentView if let current = self.doneButtonSubtitle { @@ -195,7 +156,7 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT subtitle = ComponentView() self.doneButtonSubtitle = subtitle } - + let subtitleSize = subtitle.update( transition: .immediate, component: AnyComponent( @@ -208,43 +169,36 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT environment: {}, containerSize: size ) - + if let view = subtitle.view { if view.superview == nil { view.isUserInteractionEnabled = false self.view.addSubview(view) } - + titleFrame.origin.y -= 8.0 - + let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 3.0), size: subtitleSize) view.frame = subtitleFrame } } - + self.doneButtonTitleNode.frame = titleFrame - + let _ = self.doneButtonSolidTitleNode.updateLayout(constrainedSize) self.doneButtonSolidTitleNode.frame = self.doneButtonTitleNode.frame - + self.doneButton.frame = bounds } - + var dark: Bool = false { didSet { if self.dark != oldValue { - self.doneButtonBackgroundNode.removeFromSupernode() - if self.dark { - self.doneButtonBackgroundNode = WallpaperOptionBackgroundNode(enableSaturation: true) - } else { - self.doneButtonBackgroundNode = WallpaperLightButtonBackgroundNode() - } - self.doneButtonBackgroundNode.cornerRadius = 14.0 - self.insertSubnode(self.doneButtonBackgroundNode, at: 0) + self.doneButtonBackgroundNode.isDark = self.dark } } } - + private var previousActionTime: Double? @objc func pressed() { let currentTime = CACurrentMediaTime() @@ -255,10 +209,10 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT self.action() } } - + private var theme: PresentationTheme private let strings: PresentationStrings - + public var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { didSet { self.updateThemeAndStrings(theme: self.theme, strings: self.strings) @@ -269,44 +223,42 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT self.updateThemeAndStrings(theme: self.theme, strings: self.strings) } } - + public var dark: Bool = false { didSet { self.applyButton.dark = self.dark self.applyForBothButton.dark = self.dark } } - + private let applyButton: ButtonNode private let applyForBothButton: ButtonNode - + public var cancel: (() -> Void)? public var done: ((Bool) -> Void)? - + var requiredLevel: Int? { didSet { self.applyButton.requiredLevel = self.requiredLevel } } - + public init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) { self.theme = theme self.strings = strings self.cancelButtonType = cancelButtonType self.doneButtonType = doneButtonType - + self.applyButton = ButtonNode(strings: strings) self.applyForBothButton = ButtonNode(strings: strings) - + super.init() - + self.addSubnode(self.applyButton) - if case .setPeer = doneButtonType { - self.addSubnode(self.applyForBothButton) - } - + self.addSubnode(self.applyForBothButton) + self.updateThemeAndStrings(theme: theme, strings: strings) - + self.applyButton.action = { [weak self] in if let self { self.done?(false) @@ -318,26 +270,27 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT } } } - + public func setDoneEnabled(_ enabled: Bool) { self.applyButton.setEnabled(enabled) self.applyForBothButton.setEnabled(enabled) } - + private var isSolid = false public func setDoneIsSolid(_ isSolid: Bool, transition: ContainedViewLayoutTransition) { guard self.isSolid != isSolid else { return } - + self.isSolid = isSolid self.applyButton.setIsSolid(isSolid, transition: transition) self.applyForBothButton.setIsSolid(isSolid, transition: transition) } - + public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme - + self.applyButton.isUserInteractionEnabled = true + let applyTitle: String var applyForBothTitle: String? = nil var applyForBothLocked = false @@ -358,35 +311,36 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT applyTitle = "" self.applyButton.isUserInteractionEnabled = false } - + self.applyButton.updateTitle(applyTitle, theme: theme) if let applyForBothTitle { self.applyForBothButton.updateTitle(applyForBothTitle, theme: theme) } self.applyForBothButton.isLocked = applyForBothLocked } - + public func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - let inset: CGFloat = 16.0 - let buttonHeight: CGFloat = 50.0 - + let inset: CGFloat = 30.0 + let buttonHeight: CGFloat = 52.0 + let spacing: CGFloat = 8.0 - + let applyFrame = CGRect(origin: CGPoint(x: inset, y: 2.0), size: CGSize(width: size.width - inset * 2.0, height: buttonHeight)) let applyForBothFrame = CGRect(origin: CGPoint(x: inset, y: applyFrame.maxY + spacing), size: CGSize(width: size.width - inset * 2.0, height: buttonHeight)) - + var showApplyForBothButton = false if case .setPeer = self.doneButtonType { showApplyForBothButton = true } transition.updateAlpha(node: self.applyForBothButton, alpha: showApplyForBothButton ? 1.0 : 0.0) - + self.applyForBothButton.isUserInteractionEnabled = showApplyForBothButton + self.applyButton.frame = applyFrame self.applyButton.updateSize(applyFrame.size) self.applyForBothButton.frame = applyForBothFrame self.applyForBothButton.updateSize(applyForBothFrame.size) } - + @objc func cancelPressed() { self.cancel?() } @@ -395,7 +349,7 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT public final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalleryToolbar { private var theme: PresentationTheme private let strings: PresentationStrings - + public var cancelButtonType: WallpaperGalleryToolbarCancelButtonType { didSet { self.updateThemeAndStrings(theme: self.theme, strings: self.strings) @@ -406,119 +360,135 @@ public final class WallpaperGalleryOldToolbarNode: ASDisplayNode, WallpaperGalle self.updateThemeAndStrings(theme: self.theme, strings: self.strings) } } - - private let cancelButton = HighlightTrackingButtonNode() - private let cancelHighlightBackgroundNode = ASDisplayNode() - private let doneButton = HighlightTrackingButtonNode() - private let doneHighlightBackgroundNode = ASDisplayNode() - private let backgroundNode = NavigationBackgroundNode(color: .clear) - private let separatorNode = ASDisplayNode() - private let topSeparatorNode = ASDisplayNode() - + + private let cancelButton = ComponentView() + private let doneButton = ComponentView() + + private var cancelTitle: String = "" + private var doneTitle: String = "" + private var doneEnabled: Bool = true + private var validLayout: (CGSize, ContainerViewLayout)? + public var cancel: (() -> Void)? public var done: ((Bool) -> Void)? - + public init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) { self.theme = theme self.strings = strings self.cancelButtonType = cancelButtonType self.doneButtonType = doneButtonType - - self.cancelHighlightBackgroundNode.alpha = 0.0 - self.doneHighlightBackgroundNode.alpha = 0.0 - + super.init() - self.addSubnode(self.backgroundNode) - self.addSubnode(self.cancelHighlightBackgroundNode) - self.addSubnode(self.cancelButton) - self.addSubnode(self.doneHighlightBackgroundNode) - self.addSubnode(self.doneButton) - self.addSubnode(self.separatorNode) - self.addSubnode(self.topSeparatorNode) - self.updateThemeAndStrings(theme: theme, strings: strings) - - self.cancelButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.cancelHighlightBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.cancelHighlightBackgroundNode.alpha = 1.0 - } else { - strongSelf.cancelHighlightBackgroundNode.alpha = 0.0 - strongSelf.cancelHighlightBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - } - - self.doneButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.doneHighlightBackgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.doneHighlightBackgroundNode.alpha = 1.0 - } else { - strongSelf.doneHighlightBackgroundNode.alpha = 0.0 - strongSelf.doneHighlightBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - } - - self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside) } - + public func setDoneEnabled(_ enabled: Bool) { - self.doneButton.alpha = enabled ? 1.0 : 0.4 - self.doneButton.isUserInteractionEnabled = enabled + self.doneEnabled = enabled + if let (size, layout) = self.validLayout { + self.updateLayout(size: size, layout: layout, transition: .immediate) + } } - + public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme - self.backgroundNode.updateColor(color: theme.rootController.tabBar.backgroundColor, transition: .immediate) - self.separatorNode.backgroundColor = theme.rootController.tabBar.separatorColor - self.topSeparatorNode.backgroundColor = theme.rootController.tabBar.separatorColor - self.cancelHighlightBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor - self.doneHighlightBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor - - let cancelTitle: String + switch self.cancelButtonType { case .cancel: - cancelTitle = strings.Common_Cancel + self.cancelTitle = strings.Common_Cancel case .discard: - cancelTitle = strings.WallpaperPreview_PatternPaternDiscard + self.cancelTitle = strings.WallpaperPreview_PatternPaternDiscard } - let doneTitle: String switch self.doneButtonType { case .set, .setPeer, .setChannel: - doneTitle = strings.Wallpaper_Set + self.doneTitle = strings.Wallpaper_Set case .proceed: - doneTitle = strings.Theme_Colors_Proceed + self.doneTitle = strings.Theme_Colors_Proceed case .apply: - doneTitle = strings.WallpaperPreview_PatternPaternApply + self.doneTitle = strings.WallpaperPreview_PatternPaternApply case .none: - doneTitle = "" - self.doneButton.isUserInteractionEnabled = false + self.doneTitle = "" + } + + if let (size, layout) = self.validLayout { + self.updateLayout(size: size, layout: layout, transition: .immediate) } - self.cancelButton.setTitle(cancelTitle, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: []) - self.doneButton.setTitle(doneTitle, with: Font.regular(17.0), with: theme.list.itemPrimaryTextColor, for: []) } - + public func updateLayout(size: CGSize, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.cancelButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width / 2.0), height: size.height)) - self.cancelHighlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width / 2.0), height: size.height)) - self.doneButton.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: size.width - floor(size.width / 2.0), height: size.height)) - self.doneHighlightBackgroundNode.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: size.width - floor(size.width / 2.0), height: size.height)) - self.separatorNode.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: UIScreenPixel, height: size.height + layout.intrinsicInsets.bottom)) - self.topSeparatorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel)) - self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) - self.backgroundNode.update(size: CGSize(width: size.width, height: size.height + layout.intrinsicInsets.bottom), transition: .immediate) - } - - @objc func cancelPressed() { - self.cancel?() - } - - @objc func donePressed() { - self.done?(false) + self.validLayout = (size, layout) + + let sideInset: CGFloat = 30.0 + let spacing: CGFloat = 24.0 + let buttonHeight: CGFloat = 52.0 + let buttonWidth = floor(max(1.0, size.width - sideInset * 2.0 - spacing) / 2.0) + let buttonY = floor((size.height - buttonHeight) / 2.0) + let cancelFrame = CGRect(x: sideInset, y: buttonY, width: buttonWidth, height: buttonHeight) + let doneFrame = CGRect(x: cancelFrame.maxX + spacing, y: buttonY, width: max(1.0, size.width - sideInset - cancelFrame.maxX - spacing), height: buttonHeight) + + let glassColor = UIColor(white: 1.0, alpha: 0.0) + let foregroundColor = self.theme.list.itemPrimaryTextColor + let componentTransition = ComponentTransition(transition) + + let cancelSize = self.cancelButton.update( + transition: componentTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .actualGlass, + color: glassColor, + foreground: foregroundColor, + pressedColor: glassColor, + cornerRadius: buttonHeight * 0.5 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(self.cancelTitle), + component: AnyComponent(Text(text: self.cancelTitle, font: Font.semibold(17.0), color: foregroundColor)) + ), + action: { [weak self] in + self?.cancel?() + } + )), + environment: {}, + containerSize: cancelFrame.size + ) + if let cancelView = self.cancelButton.view { + if cancelView.superview == nil { + self.view.addSubview(cancelView) + } + transition.updateFrame(view: cancelView, frame: CGRect(origin: cancelFrame.origin, size: cancelSize)) + } + + let doneIsVisible = !self.doneTitle.isEmpty + let doneSize = self.doneButton.update( + transition: componentTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .actualGlass, + color: glassColor, + foreground: foregroundColor, + pressedColor: glassColor, + cornerRadius: buttonHeight * 0.5 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(self.doneTitle), + component: AnyComponent(Text(text: self.doneTitle, font: Font.semibold(17.0), color: foregroundColor)) + ), + isEnabled: self.doneEnabled && doneIsVisible, + tintWhenDisabled: false, + action: { [weak self] in + self?.done?(false) + } + )), + environment: {}, + containerSize: doneFrame.size + ) + if let doneView = self.doneButton.view { + if doneView.superview == nil { + self.view.addSubview(doneView) + } + transition.updateFrame(view: doneView, frame: CGRect(origin: doneFrame.origin, size: doneSize)) + componentTransition.setAlpha(view: doneView, alpha: doneIsVisible ? (self.doneEnabled ? 1.0 : 0.4) : 0.0) + doneView.isUserInteractionEnabled = self.doneEnabled && doneIsVisible + } } } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift index 4129277be3..2b9aa4b32a 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperOptionButtonNode.swift @@ -5,6 +5,8 @@ import AsyncDisplayKit import SwiftSignalKit import CheckNode import AnimationUI +import ComponentFlow +import GlassBackgroundComponent public enum WallpaperOptionButtonValue { case check(Bool) @@ -68,31 +70,43 @@ final class WallpaperLightButtonBackgroundNode: ASDisplayNode { } final class WallpaperOptionBackgroundNode: ASDisplayNode { - private let backgroundNode: NavigationBackgroundNode - var enableSaturation: Bool { didSet { - self.backgroundNode.updateColor(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: self.enableSaturation, transition: .immediate) } } - init(enableSaturation: Bool = false) { + var isDark: Bool { + didSet { + if self.isDark != oldValue, let validSize = self.validSize { + self.updateLayout(size: validSize) + } + } + } + + private var validSize: CGSize? + + init(enableSaturation: Bool = false, isDark: Bool = true) { self.enableSaturation = enableSaturation - self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: enableSaturation) + self.isDark = isDark super.init() - - self.clipsToBounds = true - self.isUserInteractionEnabled = false - - self.addSubnode(self.backgroundNode) + + self.setViewBlock({ + return GlassBackgroundView() + }) + } + + private var glassView: GlassBackgroundView { + return self.view as! GlassBackgroundView } - func updateLayout(size: CGSize) { - let frame = CGRect(origin: .zero, size: size) - self.backgroundNode.frame = frame - - self.backgroundNode.update(size: size, transition: .immediate) + var contentView: UIView { + return self.glassView.contentView + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition = .immediate) { + self.validSize = size + self.glassView.update(size: size, cornerRadius: size.height * 0.5, isDark: self.isDark, tintColor: .init(kind: .panel), isInteractive: true, transition: ComponentTransition(transition)) } } @@ -105,9 +119,7 @@ final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { var enableSaturation: Bool = false { didSet { - if let backgroundNode = self.backgroundNode as? WallpaperOptionBackgroundNode { - backgroundNode.enableSaturation = self.enableSaturation - } + self.backgroundNode.enableSaturation = self.enableSaturation } } @@ -117,105 +129,70 @@ final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { if self.dark != oldValue { self.backgroundNode.removeFromSupernode() if self.dark { - self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation) + self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation, isDark: true) } else { - self.backgroundNode = WallpaperLightButtonBackgroundNode() + self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation, isDark: false) } self.insertSubnode(self.backgroundNode, at: 0) } } } - private var backgroundNode: ASDisplayNode + private var backgroundNode: WallpaperOptionBackgroundNode private let iconNode: ASImageNode private let textNode: ImmediateTextNode private var animationNode: AnimationNode? func setIcon(_ image: UIImage?) { - self.iconNode.image = generateTintedImage(image: image, color: .white) + self.iconNode.image = generateTintedImage(image: image, color: self.dark ? .white : .black) } init(content: Content, dark: Bool) { self.content = content self.dark = dark - if dark { - self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation) - } else { - self.backgroundNode = WallpaperLightButtonBackgroundNode() - } + self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation, isDark: dark) self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false self.iconNode.contentMode = .center + let iconColor: UIColor = dark ? .white : .black + var title: String switch content { case let .text(text): title = text case let .icon(icon, _): title = "" - self.iconNode.image = generateTintedImage(image: icon, color: .white) + self.iconNode.image = generateTintedImage(image: icon, color: .black) case let .dayNight(isNight): title = "" - let animationNode = AnimationNode(animation: isNight ? "anim_sun_reverse" : "anim_sun", colors: [:], scale: 1.0) + let animationNode = AnimationNode(animation: isNight ? "anim_sun_reverse" : "anim_sun", colors: ["__allcolors__": iconColor], scale: 1.0) animationNode.speed = 1.66 animationNode.isUserInteractionEnabled = false self.animationNode = animationNode } self.textNode = ImmediateTextNode() - self.textNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: .white) + self.textNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: iconColor) super.init() self.isExclusiveTouch = true self.addSubnode(self.backgroundNode) - self.addSubnode(self.iconNode) - self.addSubnode(self.textNode) + self.backgroundNode.contentView.addSubnode(self.iconNode) + self.backgroundNode.contentView.addSubnode(self.textNode) if let animationNode = self.animationNode { - self.addSubnode(animationNode) - } - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.backgroundNode.alpha = 0.4 - - strongSelf.iconNode.layer.removeAnimation(forKey: "opacity") - strongSelf.iconNode.alpha = 0.4 - - strongSelf.textNode.layer.removeAnimation(forKey: "opacity") - strongSelf.textNode.alpha = 0.4 - -// if let animationNode = strongSelf.animationNode { -// animationNode.layer.removeAnimation(forKey: "opacity") -// animationNode.alpha = 0.4 -// } - } else { - strongSelf.backgroundNode.alpha = 1.0 - strongSelf.backgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - - strongSelf.iconNode.alpha = 1.0 - strongSelf.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - - strongSelf.textNode.alpha = 1.0 - strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - -// if let animationNode = strongSelf.animationNode { -// animationNode.alpha = 1.0 -// animationNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) -// } - } - } + self.backgroundNode.contentView.addSubnode(animationNode) } } func setIsNight(_ isNight: Bool) { - self.animationNode?.setAnimation(name: !isNight ? "anim_sun_reverse" : "anim_sun", colors: [:]) + let iconColor: UIColor = self.dark ? .white : .black + self.animationNode?.setAnimation(name: !isNight ? "anim_sun_reverse" : "anim_sun", colors: ["__allcolors__": iconColor]) self.animationNode?.speed = 1.66 self.animationNode?.playOnce() @@ -225,11 +202,6 @@ final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { } } - var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) { - didSet { - } - } - private var textSize: CGSize? override func measure(_ constrainedSize: CGSize) -> CGSize { switch self.content { @@ -249,11 +221,7 @@ final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { let size = self.bounds.size self.backgroundNode.frame = self.bounds - if let backgroundNode = self.backgroundNode as? WallpaperOptionBackgroundNode { - backgroundNode.updateLayout(size: self.backgroundNode.bounds.size) - } else if let backgroundNode = self.backgroundNode as? WallpaperLightButtonBackgroundNode { - backgroundNode.updateLayout(size: self.backgroundNode.bounds.size) - } + self.backgroundNode.updateLayout(size: self.backgroundNode.bounds.size) self.backgroundNode.cornerRadius = size.height / 2.0 self.iconNode.frame = self.bounds @@ -303,7 +271,16 @@ public final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { public var title: String { didSet { - self.textNode.attributedText = NSAttributedString(string: title, font: Font.medium(13), textColor: .white) + self.textNode.attributedText = NSAttributedString(string: title, font: Font.medium(13), textColor: self.dark ? .white : .black) + } + } + + public var dark: Bool = false { + didSet { + let color: UIColor = self.dark ? .white : .black + self.checkNode.theme = CheckNodeTheme(backgroundColor: color, strokeColor: .clear, borderColor: color, overlayBorder: false, hasInset: false, hasShadow: false, borderWidth: 1.5) + self.textNode.attributedText = NSAttributedString(string: self.title, font: Font.medium(13), textColor: self.dark ? .white : .black) + let _ = self.textNode.updateLayout(CGSize(width: 160.0, height: 40.0)) } } @@ -324,8 +301,6 @@ public final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { super.init() - self.clipsToBounds = true - self.cornerRadius = 14.0 self.isExclusiveTouch = true switch value { @@ -345,32 +320,9 @@ public final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { self.addSubnode(self.backgroundNode) - self.addSubnode(self.checkNode) - self.addSubnode(self.textNode) - self.addSubnode(self.colorNode) - - self.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.backgroundNode.alpha = 0.4 - - strongSelf.colorNode.layer.removeAnimation(forKey: "opacity") - strongSelf.colorNode.alpha = 0.4 - } else { - strongSelf.backgroundNode.alpha = 1.0 - strongSelf.backgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - - strongSelf.colorNode.alpha = 1.0 - strongSelf.colorNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - } - - public var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) { - didSet { - } + self.backgroundNode.contentView.addSubnode(self.checkNode) + self.backgroundNode.contentView.addSubnode(self.textNode) + self.backgroundNode.contentView.addSubnode(self.colorNode) } public var color: UIColor? { diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift index bed529da7e..73238caf47 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperPatternPanelNode.swift @@ -293,6 +293,7 @@ public final class WallpaperPatternPanelNode: ASDisplayNode { self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.alwaysBounceHorizontal = true + self.scrollNode.view.scrollsToTop = false let sliderView = TGPhotoEditorSliderView() sliderView.disableSnapToPositions = true diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift index 242cffb640..c6ff0f4561 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerItem.swift @@ -49,12 +49,16 @@ final class ThemeColorsGridControllerItemNode: GridItemNode { self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false) super.init() + self.clipsToBounds = true + self.addSubnode(self.wallpaperNode) } override func didLoad() { super.didLoad() + self.view.layer.cornerRadius = 16.0 + self.view.isExclusiveTouch = true self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift index 315d529ec3..4bed625ea8 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridControllerNode.swift @@ -74,7 +74,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { let ready = ValuePromise() private var topBackgroundNode: ASDisplayNode - private var separatorNode: ASDisplayNode + private let maskNode: ASImageNode private let customColorItemNode: ItemListActionItemNode private var customColorItem: ItemListActionItem @@ -105,8 +105,8 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { self.topBackgroundNode = ASDisplayNode() self.topBackgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor - self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false self.customColorItemNode = ItemListActionItemNode() self.customColorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { @@ -124,12 +124,13 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { if case .default = controller.mode { self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.gridNode.addSubnode(self.topBackgroundNode) - self.gridNode.addSubnode(self.separatorNode) self.gridNode.addSubnode(self.customColorItemNode) } else { self.backgroundColor = presentationData.theme.list.plainBackgroundColor } self.addSubnode(self.gridNode) + self.gridNode.addSubnode(self.maskNode) + self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true, glass: true) let previousEntries = Atomic<[ThemeColorsGridControllerEntry]?>(value: nil) @@ -227,6 +228,27 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { } } self.gridNode.view.addGestureRecognizer(tapRecognizer) + + self.gridNode.presentationLayoutUpdated = { [weak self] gridLayout, transition in + if let strongSelf = self, let (layout, _) = strongSelf.validLayout { + let sideInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) + let maskSideInset: CGFloat = layout.size.width >= 320.0 ? sideInset : 0.0 + + let maskY: CGFloat + if let controller = strongSelf.controller, case .default = controller.mode { + let buttonTopInset: CGFloat = 32.0 + let buttonHeight: CGFloat = 44.0 + let buttonBottomInset: CGFloat = 35.0 + let buttonInset = buttonTopInset + buttonHeight + buttonBottomInset + let buttonOffset = buttonInset + 10.0 + maskY = -buttonOffset + buttonInset + } else { + maskY = 0.0 + } + + transition.updateFrame(node: strongSelf.maskNode, frame: CGRect(origin: CGPoint(x: maskSideInset, y: maskY), size: CGSize(width: layout.size.width - maskSideInset * 2.0, height: gridLayout.contentSize.height + 10.0))) + } + } } @@ -263,7 +285,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { self.rightOverlayNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor self.topBackgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor - self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true, glass: true) self.customColorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: presentationData.strings.WallpaperColors_SetCustomColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in self?.presentColorPicker() @@ -297,6 +319,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { return } let hadValidLayout = self.validLayout != nil + self.validLayout = (layout, navigationBarHeight) var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight @@ -304,42 +327,23 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { insets.right = layout.safeInsets.right let scrollIndicatorInsets = insets - let itemsPerRow: Int - if case .compact = layout.metrics.widthClass { - switch layout.orientation { - case .portrait: - itemsPerRow = 3 - case .landscape: - itemsPerRow = 5 - } - } else { - itemsPerRow = 3 - } + let padding: CGFloat = 12.0 + let minSpacing: CGFloat = 6.0 let referenceImageSize: CGSize let screenWidth = min(layout.size.width, layout.size.height) - if screenWidth >= 375.0 { + if screenWidth >= 390.0 { referenceImageSize = CGSize(width: 108.0, height: 108.0) } else { referenceImageSize = CGSize(width: 91.0, height: 91.0) } - let width = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - let imageSize: CGSize - let spacing: CGFloat - var fillWidth: Bool? - if case .peer = controller.mode { - spacing = 1.0 - - let itemWidth = floorToScreenPixels((width - spacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)) - imageSize = CGSize(width: itemWidth, height: itemWidth) - fillWidth = true - } else { - let minSpacing = 8.0 - - imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((width - CGFloat(itemsPerRow + 1) * minSpacing) / CGFloat(itemsPerRow)), height: referenceImageSize.height)) - spacing = floor((width - CGFloat(itemsPerRow) * imageSize.width) / CGFloat(itemsPerRow + 1)) - } + let sideInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) + let gridWidth = layout.size.width - sideInset * 2.0 + let imageCount = max(2, Int((gridWidth - padding * 2.0) / referenceImageSize.width)) + let itemWidth = floorToScreenPixels((gridWidth - padding * 2.0 - CGFloat(imageCount - 1) * minSpacing) / CGFloat(imageCount)) + let imageSize = CGSize(width: itemWidth, height: itemWidth) + let spacing = floorToScreenPixels((gridWidth - padding * 2.0 - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount - 1)) let buttonTopInset: CGFloat = 32.0 let buttonHeight: CGFloat = 44.0 @@ -349,26 +353,27 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { var buttonOffset = buttonInset + 10.0 var listInsets = insets - if case .default = controller.mode { - if layout.size.width >= 375.0 { - let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) - listInsets.left += inset - listInsets.right += inset - - if self.leftOverlayNode.supernode == nil { - self.gridNode.addSubnode(self.leftOverlayNode) - } - if self.rightOverlayNode.supernode == nil { - self.gridNode.addSubnode(self.rightOverlayNode) - } - } else { - if self.leftOverlayNode.supernode != nil { - self.leftOverlayNode.removeFromSupernode() - } - if self.rightOverlayNode.supernode != nil { - self.rightOverlayNode.removeFromSupernode() - } + if layout.size.width >= 320.0 { + listInsets.left = sideInset + listInsets.right = sideInset + + if self.leftOverlayNode.supernode == nil { + self.gridNode.addSubnode(self.leftOverlayNode) } + if self.rightOverlayNode.supernode == nil { + self.gridNode.addSubnode(self.rightOverlayNode) + } + } else { + if self.leftOverlayNode.supernode != nil { + self.leftOverlayNode.removeFromSupernode() + } + if self.rightOverlayNode.supernode != nil { + self.rightOverlayNode.removeFromSupernode() + } + } + + if case .default = controller.mode { + self.customColorItemNode.isHidden = false } else { self.customColorItemNode.isHidden = true buttonOffset = 0.0 @@ -381,19 +386,19 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { colorApply(false) transition.updateFrame(node: self.topBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 500.0))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonInset - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.customColorItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonTopInset), size: colorLayout.contentSize)) - self.leftOverlayNode.frame = CGRect(x: 0.0, y: -buttonOffset, width: listInsets.left, height: buttonTopInset + colorLayout.contentSize.height + UIScreenPixel) - self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + UIScreenPixel) + self.leftOverlayNode.frame = CGRect(x: 0.0, y: -buttonOffset, width: listInsets.left, height: buttonTopInset + colorLayout.contentSize.height + 10000.0) + self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + 10000.0) insets.top += spacing + buttonInset - - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: fillWidth, lineSpacing: spacing, itemSpacing: fillWidth != nil ? spacing : nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + listInsets.top = insets.top + listInsets.left += 3.0 + listInsets.right += 3.0 self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: listInsets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: nil, lineSpacing: spacing, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - self.validLayout = (layout, navigationBarHeight) if !hadValidLayout { self.dequeueTransitions() } @@ -406,7 +411,6 @@ final class ThemeColorsGridControllerNode: ASDisplayNode { self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.25, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) self.topBackgroundNode.layer.animatePosition(from: self.topBackgroundNode.layer.position.offsetBy(dx: 0.0, dy: -offset), to: self.topBackgroundNode.layer.position, duration: duration) - self.separatorNode.layer.animatePosition(from: self.separatorNode.layer.position.offsetBy(dx: 0.0, dy: -offset), to: self.separatorNode.layer.position, duration: duration) self.customColorItemNode.layer.animatePosition(from: self.customColorItemNode.layer.position.offsetBy(dx: 0.0, dy: -offset), to: self.customColorItemNode.layer.position, duration: duration) } } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift index 2c4d0213fa..da424a986a 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift @@ -115,7 +115,7 @@ public final class ThemeGridController: ViewController { if let isEmpty = self.isEmpty, isEmpty { } else { if self.editingMode { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) } else { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) } @@ -478,7 +478,7 @@ public final class ThemeGridController: ViewController { @objc func editPressed() { self.editingMode = true - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)) self.controllerNode.updateState { state in var state = state state.editing = true diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift index 13f0696dca..6cd5b48ced 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift @@ -15,7 +15,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec if let image = cachedBorderImages[key] { return image } else { - let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in + let image = generateImage(CGSize(width: 32.0, height: 32.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -41,7 +41,7 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec context.setLineWidth(lineWidth) context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) } - })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) + })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 16) cachedBorderImages[key] = image return image } @@ -113,7 +113,7 @@ final class ThemeGridControllerItemNode: GridItemNode { override func didLoad() { super.didLoad() - self.view.layer.cornerRadius = 10.0 + self.view.layer.cornerRadius = 16.0 self.view.isExclusiveTouch = true self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift index 7c2b63a292..4ca22beb39 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift @@ -320,7 +320,7 @@ final class ThemeGridControllerNode: ASDisplayNode { self.addSubnode(self.gridNode) self.gridNode.addSubnode(self.maskNode) - self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true) + self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true, glass: true) let previousEntries = Atomic<[ThemeGridControllerEntry]?>(value: nil) let interaction = ThemeGridControllerInteraction(openWallpaper: { [weak self] wallpaper in @@ -577,7 +577,7 @@ final class ThemeGridControllerNode: ASDisplayNode { let sideInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) var listInsets = layout.safeInsets - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { listInsets.left = sideInset listInsets.right = sideInset } @@ -596,7 +596,7 @@ final class ThemeGridControllerNode: ASDisplayNode { transition.updateFrame(node: strongSelf.resetDescriptionItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: gridLayout.contentSize.height + 35.0 + resetLayout.contentSize.height), size: resetDescriptionLayout.contentSize)) let maskSideInset = strongSelf.leftOverlayNode.frame.maxX - strongSelf.maskNode.frame = CGRect(origin: CGPoint(x: maskSideInset, y: strongSelf.separatorNode.frame.minY + UIScreenPixel + 4.0), size: CGSize(width: layout.size.width - sideInset * 2.0, height: gridLayout.contentSize.height + 6.0)) + strongSelf.maskNode.frame = CGRect(origin: CGPoint(x: maskSideInset, y: strongSelf.separatorNode.frame.minY + UIScreenPixel), size: CGSize(width: layout.size.width - sideInset * 2.0, height: gridLayout.contentSize.height + 10.0)) } } } @@ -766,22 +766,23 @@ final class ThemeGridControllerNode: ASDisplayNode { var scrollIndicatorInsets = insets + let padding: CGFloat = 12.0 let minSpacing: CGFloat = 6.0 + let referenceImageSize: CGSize let screenWidth = min(layout.size.width, layout.size.height) if screenWidth >= 390.0 { - referenceImageSize = CGSize(width: 112.0, height: 150.0) + referenceImageSize = CGSize(width: 110.0, height: 150.0) } else { - referenceImageSize = CGSize(width: 91.0, height: 161.0) + referenceImageSize = CGSize(width: 90.0, height: 150.0) } - let sideInset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) let gridWidth = layout.size.width - sideInset * 2.0 - let imageCount = Int((gridWidth - minSpacing * 2.0) / (referenceImageSize.width)) - let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((gridWidth - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) - let spacing = floor((gridWidth - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1)) + let imageCount = Int((gridWidth - padding * 2.0) / (referenceImageSize.width)) + let imageSize = referenceImageSize.aspectFilled(CGSize(width: floorToScreenPixels((gridWidth - padding * 2.0 - CGFloat(imageCount - 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) + let spacing = floorToScreenPixels((gridWidth - padding * 2.0 - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount - 1)) let makeColorLayout = self.colorItemNode.asyncLayout() let makeGalleryLayout = (self.galleryItemNode as? ItemListActionItemNode)?.asyncLayout() @@ -790,7 +791,7 @@ final class ThemeGridControllerNode: ASDisplayNode { let makeDescriptionLayout = self.descriptionItemNode.asyncLayout() var listInsets = insets - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { listInsets.left = sideInset listInsets.right = sideInset if self.leftOverlayNode.supernode == nil { @@ -850,10 +851,10 @@ final class ThemeGridControllerNode: ASDisplayNode { } let buttonOffset = buttonInset + 10.0 - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 504.0))) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 500.0))) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonInset - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - var originY = -buttonOffset + buttonTopInset + var originY = -buttonOffset + buttonTopInset - 4.0 if !isChannel { transition.updateFrame(node: self.colorItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: colorLayout.contentSize)) originY += colorLayout.contentSize.height @@ -876,7 +877,9 @@ final class ThemeGridControllerNode: ASDisplayNode { self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height + 10000.0) insets.top += spacing + buttonInset - listInsets.top = insets.top + listInsets.top = insets.top + 4.0 + listInsets.left += 3.0 + listInsets.right += 3.0 if self.currentState.editing { let panelHeight: CGFloat diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift deleted file mode 100644 index e3d5a34364..0000000000 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift +++ /dev/null @@ -1,257 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import ListSectionHeaderNode - -private class ThemeGridColorNode: HighlightableButtonNode { - let action: () -> Void - - init(color: WallpaperSearchColor, strokeColor: UIColor, dark: Bool, action: @escaping (WallpaperSearchColor) -> Void) { - self.action = { - action(color) - } - - super.init() - - let image: UIImage? - if color == .white && !dark { - image = generateFilledCircleImage(diameter: 42.0, color: .white, strokeColor: strokeColor, strokeWidth: 1.0) - } else if color == .black && dark { - image = generateFilledCircleImage(diameter: 42.0, color: .black, strokeColor: strokeColor, strokeWidth: 1.0) - } else { - image = generateFilledCircleImage(diameter: 42.0, color: color.displayColor) - } - self.setImage(image, for: .normal) - } - - override func didLoad() { - super.didLoad() - - self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) - } - - @objc func pressed() { - self.action() - } -} - -private let inset: CGFloat = 15.0 -private let diameter: CGFloat = 42.0 - -final class ThemeGridSearchColorsNode: ASDisplayNode { - private var theme: PresentationTheme - private var strings: PresentationStrings - private let sectionHeaderNode: ListSectionHeaderNode - private let scrollNode: ASScrollNode - - private let colorSelected: (WallpaperSearchColor) -> Void - - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, colorSelected: @escaping (WallpaperSearchColor) -> Void) { - self.theme = theme - self.strings = strings - - self.colorSelected = colorSelected - - self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) - self.sectionHeaderNode.title = strings.WallpaperSearch_ColorTitle.uppercased() - - self.scrollNode = ASScrollNode() - self.scrollNode.view.showsHorizontalScrollIndicator = false - self.scrollNode.view.showsVerticalScrollIndicator = false - self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true - - super.init() - - self.addSubnode(self.sectionHeaderNode) - self.addSubnode(self.scrollNode) - - self.scrollNode.view.contentSize = CGSize(width: (inset + diameter) * CGFloat(WallpaperSearchColor.allCases.count) + inset, height: 71.0) - - for color in WallpaperSearchColor.allCases { - let colorNode = ThemeGridColorNode(color: color, strokeColor: theme.list.controlSecondaryColor, dark: theme.overallDarkAppearance, action: colorSelected) - self.scrollNode.addSubnode(colorNode) - } - } - - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - if self.theme !== theme || self.strings !== strings { - self.theme = theme - self.strings = strings - - self.sectionHeaderNode.title = strings.WallpaperSearch_ColorTitle.uppercased() - self.sectionHeaderNode.updateTheme(theme: theme) - } - } - - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: constrainedSize.width, height: 100.0) - } - - private var validLayout: (CGSize, CGFloat, CGFloat)? - func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) { - let hadLayout = self.validLayout != nil - self.validLayout = (size, leftInset, rightInset) - - self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 29.0)) - self.sectionHeaderNode.updateLayout(size: CGSize(width: size.width, height: 29.0), leftInset: leftInset, rightInset: rightInset) - - var insets = UIEdgeInsets() - insets.left += leftInset - insets.right += rightInset - - self.scrollNode.frame = CGRect(x: 0.0, y: 29.0, width: size.width, height: size.height - 29.0) - self.scrollNode.view.contentInset = insets - if !hadLayout { - self.scrollNode.view.contentOffset = CGPoint(x: -leftInset, y: 0.0) - } - - var offset: CGFloat = inset - if let subnodes = self.scrollNode.subnodes { - for node in subnodes { - node.frame = CGRect(x: offset, y: inset, width: diameter, height: diameter) - offset += diameter + inset - } - } - } -} - - -class ThemeGridSearchColorsItem: ListViewItem { - let account: Account - let theme: PresentationTheme - let strings: PresentationStrings - let colorSelected: (WallpaperSearchColor) -> Void - - let header: ListViewItemHeader? - - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, colorSelected: @escaping (WallpaperSearchColor) -> Void) { - self.account = account - self.theme = theme - self.strings = strings - self.colorSelected = colorSelected - self.header = nil - } - - 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 = ThemeGridSearchColorsItemNode() - let makeLayout = node.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem != nil) - node.contentSize = nodeLayout.contentSize - node.insets = nodeLayout.insets - - completion(node, nodeApply) - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? ThemeGridSearchColorsItemNode { - let layout = nodeValue.asyncLayout() - async { - let (nodeLayout, apply) = layout(self, params, nextItem != nil) - Queue.mainQueue().async { - completion(nodeLayout, { info in - apply().1(info) - }) - } - } - } - } - } -} - -class ThemeGridSearchColorsItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode - private var colorsNode: ThemeGridSearchColorsNode? - - private var item: ThemeGridSearchColorsItem? - - required init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - - super.init(layerBacked: false) - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - } - - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - if let item = self.item { - let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil) - self.contentSize = nodeLayout.contentSize - self.insets = nodeLayout.insets - let _ = nodeApply() - } - } - - func asyncLayout() -> (_ item: ThemeGridSearchColorsItem, _ params: ListViewItemLayoutParams, _ last: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { - let currentItem = self.item - - return { [weak self] item, params, last in - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 101.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) - - return (nodeLayout, { [weak self] in - var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - } - - return (nil, { _ in - if let strongSelf = self { - strongSelf.item = item - - if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor - } - - let colorsNode: ThemeGridSearchColorsNode - if let currentColorsNode = strongSelf.colorsNode { - colorsNode = currentColorsNode - colorsNode.updateThemeAndStrings(theme: item.theme, strings: item.strings) - } else { - colorsNode = ThemeGridSearchColorsNode(account: item.account, theme: item.theme, strings: item.strings, colorSelected: item.colorSelected) - strongSelf.colorsNode = colorsNode - strongSelf.addSubnode(colorsNode) - } - - colorsNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize) - colorsNode.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - - let separatorHeight = UIScreenPixel - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) - strongSelf.separatorNode.isHidden = true - } - }) - }) - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) - } - - override public func headers() -> [ListViewItemHeader]? { - if let item = self.item { - return item.header.flatMap { [$0] } - } else { - return nil - } - } -} diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchContentNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchContentNode.swift deleted file mode 100644 index 19b3e8a5c2..0000000000 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchContentNode.swift +++ /dev/null @@ -1,833 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import MergeLists -import AccountContext -import SearchUI -import ChatListSearchItemHeader -import WebSearchUI -import SearchBarNode - -enum WallpaperSearchColor: CaseIterable { - case blue - case red - case orange - case yellow - case green - case teal - case purple - case pink - case brown - case black - case gray - case white - - var string: String { - switch self { - case .blue: - return "Blue" - case .red: - return "Red" - case .orange: - return "Orange" - case .yellow: - return "Yellow" - case .green: - return "Green" - case .teal: - return "Teal" - case .purple: - return "Purple" - case .pink: - return "Pink" - case .brown: - return "Brown" - case .black: - return "Black" - case .gray: - return "Gray" - case .white: - return "White" - } - } - - var displayColor: UIColor { - switch self { - case .blue: - return UIColor(rgb: 0x0076ff) - case .red: - return UIColor(rgb: 0xff0000) - case .orange: - return UIColor(rgb: 0xff8a00) - case .yellow: - return UIColor(rgb: 0xffca00) - case .green: - return UIColor(rgb: 0x00e432) - case .teal: - return UIColor(rgb: 0x1fa9ab) - case .purple: - return UIColor(rgb: 0x7300aa) - case .pink: - return UIColor(rgb: 0xf9bec5) - case .brown: - return UIColor(rgb: 0x734021) - case .black: - return UIColor(rgb: 0x000000) - case .gray: - return UIColor(rgb: 0x5c585f) - case .white: - return UIColor(rgb: 0xffffff) - } - } - - func localizedString(strings: PresentationStrings) -> String { - switch self { - case .blue: - return strings.WallpaperSearch_ColorBlue - case .red: - return strings.WallpaperSearch_ColorRed - case .orange: - return strings.WallpaperSearch_ColorOrange - case .yellow: - return strings.WallpaperSearch_ColorYellow - case .green: - return strings.WallpaperSearch_ColorGreen - case .teal: - return strings.WallpaperSearch_ColorTeal - case .purple: - return strings.WallpaperSearch_ColorPurple - case .pink: - return strings.WallpaperSearch_ColorPink - case .brown: - return strings.WallpaperSearch_ColorBrown - case .black: - return strings.WallpaperSearch_ColorBlack - case .gray: - return strings.WallpaperSearch_ColorGray - case .white: - return strings.WallpaperSearch_ColorWhite - } - } -} - -enum WallpaperSearchQuery: Equatable { - case generic(String) - case color(WallpaperSearchColor, String) - - var botQuery: String { - switch self { - case let .generic(query): - return query - case let .color(color, query): - return "#color\(color.string) \(query)" - } - } - - var query: String { - switch self { - case let .generic(query), let .color(_, query): - return query - } - } - - func updatedWithText(_ text: String) -> WallpaperSearchQuery { - switch self { - case .generic: - return .generic(text) - case let .color(color, _): - return .color(color, text) - } - } - - func updatedWithColor(_ color: WallpaperSearchColor?) -> WallpaperSearchQuery { - if let color = color { - switch self { - case let .generic(text): - return .color(color, text) - case let .color(_, text): - return .color(color, text) - } - } else { - switch self { - case .generic: - return self - case let .color(_, text): - return .generic(text) - } - } - } -} - -final class ThemeGridSearchInteraction { - let openResult: (ChatContextResult) -> Void - let selectColor: (WallpaperSearchColor) -> Void - let setSearchQuery: (WallpaperSearchQuery) -> Void - let deleteRecentQuery: (String) -> Void - - init(openResult: @escaping (ChatContextResult) -> Void, selectColor: @escaping (WallpaperSearchColor) -> Void, setSearchQuery: @escaping (WallpaperSearchQuery) -> Void, deleteRecentQuery: @escaping (String) -> Void) { - self.openResult = openResult - self.selectColor = selectColor - self.setSearchQuery = setSearchQuery - self.deleteRecentQuery = deleteRecentQuery - } -} - -private enum ThemeGridRecentEntryStableId: Hashable { - case colors - case query(String) -} - -private enum ThemeGridRecentEntry: Comparable, Identifiable { - case colors(PresentationTheme, PresentationStrings) - case query(Int, String) - - var stableId: ThemeGridRecentEntryStableId { - switch self { - case .colors: - return .colors - case let .query(_, query): - return .query(query) - } - } - - static func ==(lhs: ThemeGridRecentEntry, rhs: ThemeGridRecentEntry) -> Bool { - switch lhs { - case let .colors(lhsTheme, lhsStrings): - if case let .colors(rhsTheme, rhsStrings) = rhs { - if lhsTheme !== rhsTheme { - return false - } - if lhsStrings !== rhsStrings { - return false - } - return true - } else { - return false - } - case let .query(lhsIndex, lhsQuery): - if case .query(lhsIndex, lhsQuery) = rhs { - return true - } else { - return false - } - } - } - - static func <(lhs: ThemeGridRecentEntry, rhs: ThemeGridRecentEntry) -> Bool { - switch lhs { - case .colors: - return true - case let .query(lhsIndex, _): - switch rhs { - case .colors: - return false - case let .query(rhsIndex, _): - return lhsIndex <= rhsIndex - } - } - } - - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ThemeGridSearchInteraction, header: ListViewItemHeader) -> ListViewItem { - switch self { - case let .colors(theme, strings): - return ThemeGridSearchColorsItem(account: account, theme: theme, strings: strings, colorSelected: { color in - interaction.selectColor(color) - }) - case let .query(_, query): - return WebSearchRecentQueryItem(account: account, theme: theme, strings: strings, query: query, tapped: { query in - interaction.setSearchQuery(.generic(query)) - }, deleted: { query in - interaction.deleteRecentQuery(query) - }, header: header) - } - } -} - -private struct ThemeGridSearchContainerRecentTransition { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] -} - -private struct ThemeGridSearchEntry: Comparable, Identifiable { - let index: Int - let result: ChatContextResult - - static func ==(lhs: ThemeGridSearchEntry, rhs: ThemeGridSearchEntry) -> Bool { - return lhs.index == rhs.index && lhs.result == rhs.result - } - - static func <(lhs: ThemeGridSearchEntry, rhs: ThemeGridSearchEntry) -> Bool { - return lhs.index < rhs.index - } - - var stableId: Int { - return self.index - } - - func item(account: Account, theme: PresentationTheme, interaction: ThemeGridSearchInteraction) -> ThemeGridSearchItem { - return ThemeGridSearchItem(account: account, theme: theme, result: self.result, interaction: interaction) - } -} - -struct ThemeGridSearchContainerTransition { - let deletions: [Int] - let insertions: [GridNodeInsertItem] - let updates: [GridNodeUpdateItem] - let displayingResults: Bool - let isEmpty: Bool - let query: String -} - -private func themeGridSearchContainerPreparedRecentTransition(from fromEntries: [ThemeGridRecentEntry], to toEntries: [ThemeGridRecentEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ThemeGridSearchInteraction, header: ListViewItemHeader) -> ThemeGridSearchContainerRecentTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) } - - return ThemeGridSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) -} - -private func themeGridSearchContainerPreparedTransition(from fromEntries: [ThemeGridSearchEntry], to toEntries: [ThemeGridSearchEntry], displayingResults: Bool, account: Account, theme: PresentationTheme, isEmpty: Bool, query: String, interaction: ThemeGridSearchInteraction) -> ThemeGridSearchContainerTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, interaction: interaction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, interaction: interaction)) } - - return ThemeGridSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, query: query) -} - -private struct ThemeGridSearchResult { - let query: String - let collection: ChatContextResultCollection - let items: [ChatContextResult] - let nextOffset: String? -} - -private struct ThemeGridSearchContext { - let result: ThemeGridSearchResult - let loadMoreIndex: String? -} - -final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { - private let context: AccountContext - - private let recentListNode: ListView - private let gridNode: GridNode - private let dimNode: ASDisplayNode - - private let emptyResultsTitleNode: ImmediateTextNode - private let emptyResultsTextNode: ImmediateTextNode - - private var enqueuedRecentTransitions: [(ThemeGridSearchContainerRecentTransition, Bool)] = [] - private var enqueuedTransitions: [(ThemeGridSearchContainerTransition, Bool)] = [] - private var validLayout: (ContainerViewLayout, CGFloat)? - - private var queryValue: WallpaperSearchQuery = .generic("") - private let queryPromise: Promise - private let searchDisposable = MetaDisposable() - private var recentDisposable: Disposable? - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - private let presentationDataPromise: Promise - - private let _isSearching = ValuePromise(false, ignoreRepeated: true) - override var isSearching: Signal { - return self._isSearching.get() - } - - init(context: AccountContext, openResult: @escaping (ChatContextResult) -> Void) { - self.context = context - self.queryPromise = Promise(self.queryValue) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.presentationData = presentationData - self.presentationDataPromise = Promise(self.presentationData) - - self.dimNode = ASDisplayNode() - self.recentListNode = ListViewImpl() - self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor - self.recentListNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - self.gridNode = GridNode() - - self.emptyResultsTitleNode = ImmediateTextNode() - self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.SharedMedia_SearchNoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) - self.emptyResultsTitleNode.textAlignment = .center - self.emptyResultsTitleNode.isHidden = true - - self.emptyResultsTextNode = ImmediateTextNode() - self.emptyResultsTextNode.maximumNumberOfLines = 0 - self.emptyResultsTextNode.textAlignment = .center - self.emptyResultsTextNode.isHidden = true - - super.init() - - self.dimNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor - - self.backgroundColor = self.presentationData.theme.chatList.backgroundColor - - self.addSubnode(self.dimNode) - self.addSubnode(self.recentListNode) - self.addSubnode(self.gridNode) - - self.addSubnode(self.emptyResultsTitleNode) - self.addSubnode(self.emptyResultsTextNode) - - let searchContext = Promise(nil) - let searchContextValue = Atomic(value: nil) - let updateSearchContext: ((ThemeGridSearchContext?) -> (ThemeGridSearchContext?, Bool)) -> Void = { f in - var shouldUpdate = false - let updated = searchContextValue.modify { current in - let (u, s) = f(current) - shouldUpdate = s - if s { - return u - } else { - return current - } - } - if shouldUpdate { - searchContext.set(.single(updated)) - } - } - - self.gridNode.isHidden = true - self.gridNode.visibleItemsUpdated = { visibleItems in - if let bottom = visibleItems.bottom { - if let context = searchContextValue.with({ $0 }), bottom.0 >= context.result.items.count - 8 { - updateSearchContext { previous in - guard let previous = previous else { - return (nil, false) - } - if previous.loadMoreIndex != nil { - return (previous, false) - } - guard let _ = previous.result.items.last else { - return (previous, false) - } - return (ThemeGridSearchContext(result: previous.result, loadMoreIndex: previous.result.nextOffset), true) - } - } - } - } - self.recentListNode.isHidden = false - - let previousSearchItems = Atomic<[ThemeGridSearchEntry]?>(value: nil) - - let interaction = ThemeGridSearchInteraction(openResult: { [weak self] result in - openResult(result) - if let strongSelf = self { - strongSelf.dismissInput?() - - let query = strongSelf.queryValue.query - if !query.isEmpty { - let _ = addRecentWallpaperSearchQuery(engine: strongSelf.context.engine, string: query).start() - } - } - }, selectColor: { [weak self] color in - self?.updateQuery({ $0.updatedWithColor(color) }, updateInterface: true) - }, setSearchQuery: { [weak self] query in - self?.dismissInput?() - self?.updateQuery({ _ in - return query - }, updateInterface: true) - }, deleteRecentQuery: { query in - let _ = removeRecentWallpaperSearchQuery(engine: context.engine, string: query).start() - }) - - let configuration = self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - - let foundItems = self.queryPromise.get() - |> mapToSignal { query -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in - let query = query.botQuery - guard !query.isEmpty else { - return .single(nil) - } - - let wallpaperQuery = "#wallpaper \(query)" - updateSearchContext { _ in - return (nil, true) - } - - return .single(([], true)) - |> then( - configuration - |> mapToSignal { configuration -> Signal in - guard let name = configuration.imageBotUsername else { - return .single(nil) - } - return context.engine.peers.resolvePeerByName(name: name, referrer: nil) - |> mapToSignal { result -> Signal in - guard case let .result(result) = result else { - return .complete() - } - return .single(result) - } - |> mapToSignal { peer -> Signal in - if let peer = peer { - return .single(peer._asPeer()) - } else { - return .single(nil) - } - } - } - |> mapToSignal { peer -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in - if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let loadMore = searchContext.get() - |> mapToSignal { searchContext -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in - if let searchContext = searchContext { - if let _ = searchContext.loadMoreIndex, let nextOffset = searchContext.result.nextOffset { - let collection = searchContext.result.collection - let geoPoint = collection.geoPoint.flatMap { geoPoint -> (Double, Double) in - return (geoPoint.latitude, geoPoint.longitude) - } - return self.context.engine.messages.requestChatContextResults(botId: collection.botId, peerId: collection.peerId, query: searchContext.result.query, location: .single(geoPoint), offset: nextOffset) - |> map { results -> ChatContextResultCollection? in - return results?.results - } - |> `catch` { error -> Signal in - return .single(nil) - } - |> map { nextResults -> (ChatContextResultCollection, String?) in - var results: [ChatContextResult] = [] - var existingIds = Set() - for result in searchContext.result.items { - results.append(result) - existingIds.insert(result.id) - } - var nextOffset: String? - if let nextResults = nextResults { - for result in nextResults.results { - if !existingIds.contains(result.id) { - results.append(result) - existingIds.insert(result.id) - } - } - if let newNextOffset = nextResults.nextOffset, !newNextOffset.isEmpty { - nextOffset = newNextOffset - } - } - let merged = ChatContextResultCollection(botId: collection.botId, peerId: collection.peerId, query: collection.query, geoPoint: collection.geoPoint, queryId: nextResults?.queryId ?? collection.queryId, nextOffset: nextOffset ?? "", presentation: collection.presentation, switchPeer: collection.switchPeer, webView: collection.webView, results: results, cacheTimeout: collection.cacheTimeout) - return (merged, nextOffset) - } - |> mapToSignal { newCollection, nextOffset -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in - updateSearchContext { previous in - return (ThemeGridSearchContext(result: ThemeGridSearchResult(query: searchContext.result.query, collection: newCollection, items: newCollection.results, nextOffset: nextOffset), loadMoreIndex: nil), true) - } - return .complete() - } - } else { - var entries: [ThemeGridSearchEntry] = [] - var i = 0 - for result in searchContext.result.items { - entries.append(ThemeGridSearchEntry(index: i, result: result)) - i += 1 - } - return .single((entries, false)) - } - } else { - return .complete() - } - } - - - return (.complete() |> delay(0.1, queue: Queue.concurrentDefaultQueue())) - |> then( - requestContextResults(engine: context.engine, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16) - |> map { results -> ChatContextResultCollection? in - return results?.results - } - |> map { collection -> ([ThemeGridSearchEntry], Bool)? in - guard let collection = collection else { - return nil - } - var entries: [ThemeGridSearchEntry] = [] - var i = 0 - for result in collection.results { - entries.append(ThemeGridSearchEntry(index: i, result: result)) - i += 1 - } - updateSearchContext { _ in - return (ThemeGridSearchContext(result: ThemeGridSearchResult(query: wallpaperQuery, collection: collection, items: collection.results, nextOffset: collection.nextOffset), loadMoreIndex: nil), true) - } - return (entries, false) - } - |> delay(0.2, queue: Queue.concurrentDefaultQueue()) - |> then(loadMore) - ) - } else { - return .single(nil) - } - } - ) - } - - let previousRecentItems = Atomic<[ThemeGridRecentEntry]?>(value: nil) - self.recentDisposable = (combineLatest(wallpaperSearchRecentQueries(engine: self.context.engine), self.presentationDataPromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] queries, presentationData in - if let strongSelf = self { - var entries: [ThemeGridRecentEntry] = [] - - entries.append(.colors(presentationData.theme, presentationData.strings)) - for i in 0 ..< queries.count { - entries.append(.query(i, queries[i])) - } - - let header = ChatListSearchItemHeader(type: .recentPeers, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.WebSearch_RecentSectionClear, action: { _ in - let _ = clearRecentWallpaperSearchQueries(engine: strongSelf.context.engine).start() - }) - - let previousEntries = previousRecentItems.swap(entries) - let transition = themeGridSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: context.account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction, header: header) - strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil) - } - }) - - self.searchDisposable.set((combineLatest(foundItems, self.presentationDataPromise.get(), self.queryPromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] entriesAndFlags, presentationData, query in - if let strongSelf = self { - strongSelf._isSearching.set(entriesAndFlags?.1 ?? false) - - let previousEntries = previousSearchItems.swap(entriesAndFlags?.0) - - var isEmpty = false - if let entriesAndFlags = entriesAndFlags { - isEmpty = entriesAndFlags.0.isEmpty && !entriesAndFlags.1 - } - - let firstTime = previousEntries == nil - let transition = themeGridSearchContainerPreparedTransition(from: previousEntries ?? [], to: entriesAndFlags?.0 ?? [], displayingResults: entriesAndFlags?.0 != nil, account: context.account, theme: presentationData.theme, isEmpty: isEmpty, query: query.query, interaction: interaction) - strongSelf.enqueueTransition(transition, firstTime: firstTime) - } - })) - - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - - strongSelf.presentationData = presentationData - strongSelf.presentationDataPromise.set(.single(presentationData)) - - if previousTheme !== presentationData.theme { - strongSelf.updateTheme(theme: presentationData.theme) - } - } - }) - - self.recentListNode.beganInteractiveDragging = { [weak self] _ in - self?.dismissInput?() - } - - self.gridNode.scrollingInitiated = { [weak self] in - self?.dismissInput?() - } - } - - override func didLoad() { - super.didLoad() - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.cancel?() - } - } - - deinit { - self.searchDisposable.dispose() - self.recentDisposable?.dispose() - self.presentationDataDisposable?.dispose() - } - - private func updateTheme(theme: PresentationTheme) { - self.backgroundColor = theme.chatList.backgroundColor - self.dimNode.backgroundColor = theme.chatList.backgroundColor - self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor - } - - private func updateQuery(_ f: (WallpaperSearchQuery) -> (WallpaperSearchQuery), updateInterface: Bool = false) { - let query = f(self.queryValue) - if query != self.queryValue { - self.queryValue = query - self.queryPromise.set(.single(query)) - - if updateInterface { - let tokens: [SearchBarToken] - let text: String - let placeholder: String - switch query { - case let .generic(query): - tokens = [] - text = query - placeholder = self.presentationData.strings.Wallpaper_Search - case let .color(color, query): - let backgroundColor = color.displayColor - let foregroundColor: UIColor - let strokeColor: UIColor - if color == .white { - foregroundColor = .black - strokeColor = self.presentationData.theme.rootController.navigationSearchBar.inputClearButtonColor - } else { - foregroundColor = .white - strokeColor = color.displayColor - } - tokens = [SearchBarToken(id: 0, icon: UIImage(bundleImageName: "Settings/WallpaperSearchColorIcon"), title: color.localizedString(strings: self.presentationData.strings), style: SearchBarToken.Style(backgroundColor: backgroundColor, foregroundColor: foregroundColor, strokeColor: strokeColor), permanent: false)] - text = query - placeholder = self.presentationData.strings.Wallpaper_SearchShort - } - self.setQuery?(nil, tokens, text) - self.setPlaceholder?(placeholder) - } - } - } - - override func searchTextUpdated(text: String) { - self.updateQuery({ $0.updatedWithText(text) }) - } - - override func searchTextClearPrefix() { - self.updateQuery({ $0.updatedWithColor(nil) }, updateInterface: true) - } - - override func searchTextClearTokens() { - self.updateQuery({ $0.updatedWithColor(nil) }, updateInterface: true) - } - - private func enqueueRecentTransition(_ transition: ThemeGridSearchContainerRecentTransition, firstTime: Bool) { - self.enqueuedRecentTransitions.append((transition, firstTime)) - - if self.validLayout != nil { - while !self.enqueuedRecentTransitions.isEmpty { - self.dequeueRecentTransition() - } - } - } - - private func dequeueRecentTransition() { - if let (transition, firstTime) = self.enqueuedRecentTransitions.first { - self.enqueuedRecentTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - if firstTime { - options.insert(.PreferSynchronousDrawing) - } else { - options.insert(.AnimateInsertion) - } - - self.recentListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in - }) - } - } - - private func enqueueTransition(_ transition: ThemeGridSearchContainerTransition, firstTime: Bool) { - self.enqueuedTransitions.append((transition, firstTime)) - - if self.validLayout != nil { - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - } - - private func dequeueTransition() { - if let (transition, _) = self.enqueuedTransitions.first { - self.enqueuedTransitions.remove(at: 0) - - let displayingResults = transition.displayingResults - self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.gridNode.isHidden = !displayingResults - strongSelf.recentListNode.isHidden = displayingResults - strongSelf.dimNode.isHidden = displayingResults - strongSelf.backgroundColor = strongSelf.presentationData.theme.chatList.backgroundColor - - strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.WebSearch_SearchNoResultsDescription(transition.query).string, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) - - let emptyResults = displayingResults && transition.isEmpty - strongSelf.emptyResultsTitleNode.isHidden = !emptyResults - strongSelf.emptyResultsTextNode.isHidden = !emptyResults - - if let (layout, navigationBarHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - } - }) - } - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - - let hadValidLayout = self.validLayout != nil - self.validLayout = (layout, navigationBarHeight) - - let minSpacing: CGFloat = 8.0 - let referenceImageSize: CGSize - let screenWidth = min(layout.size.width, layout.size.height) - if screenWidth >= 390.0 { - referenceImageSize = CGSize(width: 108.0, height: 230.0) - } else { - referenceImageSize = CGSize(width: 91.0, height: 161.0) - } - let imageCount = Int((layout.size.width - minSpacing * 2.0) / (referenceImageSize.width + minSpacing)) - let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) - let spacing = floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1)) - - let topInset = navigationBarHeight - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) - - let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - - self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - - self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight + spacing, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: nil, lineSpacing: spacing, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - - let padding: CGFloat = 16.0 - let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) - - let insets = layout.insets(options: [.input]) - let emptyTextSpacing: CGFloat = 8.0 - let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing - let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0) - - transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize)) - transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) - - if !hadValidLayout { - while !self.enqueuedRecentTransitions.isEmpty { - self.dequeueRecentTransition() - } - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - } - - private func clearRecentSearch() { - let _ = (self.context.engine.peers.clearRecentlySearchedPeers() |> deliverOnMainQueue).start() - } - - override func scrollToTop() { - if !self.gridNode.isHidden { - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.25, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - } else { - self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } - } -} diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchItem.swift deleted file mode 100644 index 4913bcdb8b..0000000000 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchItem.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation -import UIKit -import Display -import TelegramCore -import SwiftSignalKit -import AsyncDisplayKit -import TelegramPresentationData -import PhotoResources - -final class ThemeGridSearchItem: GridItem { - let account: Account - let theme: PresentationTheme - let result: ChatContextResult - let interaction: ThemeGridSearchInteraction - - let section: GridSection? = nil - - init(account: Account, theme: PresentationTheme, result: ChatContextResult, interaction: ThemeGridSearchInteraction) { - self.account = account - self.theme = theme - self.result = result - self.interaction = interaction - } - - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { - let node = ThemeGridSearchItemNode() - node.setup(item: self) - return node - } - - func update(node: GridItemNode) { - guard let node = node as? ThemeGridSearchItemNode else { - assertionFailure() - return - } - node.setup(item: self) - } -} - -final class ThemeGridSearchItemNode: GridItemNode { - private let imageNode: TransformImageNode - - private(set) var item: ThemeGridSearchItem? - private var currentDimensions: CGSize? - - override init() { - self.imageNode = TransformImageNode() - self.imageNode.contentAnimations = [.subsequentUpdates] - self.imageNode.displaysAsynchronously = false - - super.init() - - self.addSubnode(self.imageNode) - } - - override func didLoad() { - super.didLoad() - - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - } - - func setup(item: ThemeGridSearchItem) { - if self.item !== item { - var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - - var thumbnailDimensions: CGSize? - var thumbnailResource: TelegramMediaResource? - var imageResource: TelegramMediaResource? - var imageDimensions: CGSize? - var immediateThumbnailData: Data? - switch item.result { - case let .externalReference(externalReference): - if let content = externalReference.content, externalReference.type != "gif" { - imageResource = content.resource - } else if let thumbnail = externalReference.thumbnail { - imageResource = thumbnail.resource - } - imageDimensions = externalReference.content?.dimensions?.cgSize - case let .internalReference(internalReference): - if let image = internalReference.image { - immediateThumbnailData = image.immediateThumbnailData - if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 321, height: 321)) { - imageResource = representation.resource - imageDimensions = representation.dimensions.cgSize - } - if let file = internalReference.file { - if let thumbnailRepresentation = smallestImageRepresentation(file.previewRepresentations) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } else { - if let thumbnailRepresentation = smallestImageRepresentation(image.representations) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } - } else if let file = internalReference.file { - immediateThumbnailData = file.immediateThumbnailData - if let dimensions = file.dimensions { - imageDimensions = dimensions.cgSize - } else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) { - imageDimensions = largestRepresentation.dimensions.cgSize - } - imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource - } - } - - var representations: [TelegramMediaImageRepresentation] = [] - if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) - } - if let imageResource = imageResource, let imageDimensions = imageDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) - } - if !representations.isEmpty { - let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - updateImageSignal = mediaGridMessagePhoto(account: item.account, userLocation: .other, photoReference: .standalone(media: tmpImage), fullRepresentationSize: CGSize(width: 512, height: 512)) - } else { - updateImageSignal = .complete() - } - - if let updateImageSignal = updateImageSignal { - self.imageNode.setSignal(updateImageSignal) - } - - self.currentDimensions = imageDimensions - if let _ = imageDimensions { - self.setNeedsLayout() - } - } - - self.item = item - } - - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if let item = self.item { - item.interaction.openResult(item.result) - } - } - } - - override func layout() { - super.layout() - - let bounds = self.bounds - self.imageNode.frame = bounds - - if let item = self.item, let dimensions = self.currentDimensions { - let imageSize = dimensions.aspectFilled(bounds.size) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: bounds.size, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor))() - } - } -} diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperSearchRecentQueries.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperSearchRecentQueries.swift deleted file mode 100644 index 3370f59479..0000000000 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperSearchRecentQueries.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation -import TelegramCore -import SwiftSignalKit -import TelegramUIPreferences - -private struct WallpaperSearchRecentQueryItemId { - public let rawValue: EngineMemoryBuffer - - var value: String { - return String(data: self.rawValue.makeData(), encoding: .utf8) ?? "" - } - - init(_ rawValue: EngineMemoryBuffer) { - self.rawValue = rawValue - } - - init?(_ value: String) { - if let data = value.data(using: .utf8) { - self.rawValue = EngineMemoryBuffer(data: data) - } else { - return nil - } - } -} - -public final class RecentWallpaperSearchQueryItem: Codable { - public init() { - } - - public init(from decoder: Decoder) throws { - } - - public func encode(to encoder: Encoder) throws { - } -} - -func addRecentWallpaperSearchQuery(engine: TelegramEngine, string: String) -> Signal { - if let itemId = WallpaperSearchRecentQueryItemId(string) { - return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries, id: itemId.rawValue, item: RecentWallpaperSearchQueryItem(), removeTailIfCountExceeds: 100) - } else { - return .complete() - } -} - -func removeRecentWallpaperSearchQuery(engine: TelegramEngine, string: String) -> Signal { - if let itemId = WallpaperSearchRecentQueryItemId(string) { - return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries, id: itemId.rawValue) - } else { - return .complete() - } -} - -func clearRecentWallpaperSearchQueries(engine: TelegramEngine) -> Signal { - return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries) -} - -func wallpaperSearchRecentQueries(engine: TelegramEngine) -> Signal<[String], NoError> { - return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.wallpaperSearchRecentQueries)) - |> map { items -> [String] in - var result: [String] = [] - for item in items { - let value = WallpaperSearchRecentQueryItemId(item.id).value - result.append(value) - } - return result - } -} diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index a9c17a2fa4..848ae4f389 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -2766,9 +2766,9 @@ final class ShareWithPeersScreenComponent: Component { transition: transition, component: AnyComponent(GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", diff --git a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift index d9cffb9fe4..204e0d7fa3 100644 --- a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift +++ b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift @@ -42,11 +42,21 @@ public final class SliderComponent: Component { public final class Continuous: Equatable { public let value: CGFloat public let minValue: CGFloat? + public let range: ClosedRange + public let startValue: CGFloat public let valueUpdated: (CGFloat) -> Void - public init(value: CGFloat, minValue: CGFloat? = nil, valueUpdated: @escaping (CGFloat) -> Void) { + public init( + value: CGFloat, + minValue: CGFloat? = nil, + range: ClosedRange = 0.0 ... 1.0, + startValue: CGFloat = 0.0, + valueUpdated: @escaping (CGFloat) -> Void + ) { self.value = value self.minValue = minValue + self.range = range + self.startValue = startValue self.valueUpdated = valueUpdated } @@ -57,6 +67,12 @@ public final class SliderComponent: Component { if lhs.minValue != rhs.minValue { return false } + if lhs.range != rhs.range { + return false + } + if lhs.startValue != rhs.startValue { + return false + } return true } } @@ -73,6 +89,10 @@ public final class SliderComponent: Component { public let minTrackForegroundColor: UIColor? public let knobSize: CGFloat? public let knobColor: UIColor? + public let isEnabled: Bool + public let trackHeight: CGFloat? + public let displaysBorderOnTracking: Bool + public let useLegacyKnob: Bool public let isTrackingUpdated: ((Bool) -> Void)? public init( @@ -83,6 +103,10 @@ public final class SliderComponent: Component { minTrackForegroundColor: UIColor? = nil, knobSize: CGFloat? = nil, knobColor: UIColor? = nil, + isEnabled: Bool = true, + trackHeight: CGFloat? = nil, + displaysBorderOnTracking: Bool = false, + useLegacyKnob: Bool = false, isTrackingUpdated: ((Bool) -> Void)? = nil ) { self.content = content @@ -92,6 +116,10 @@ public final class SliderComponent: Component { self.minTrackForegroundColor = minTrackForegroundColor self.knobSize = knobSize self.knobColor = knobColor + self.isEnabled = isEnabled + self.trackHeight = trackHeight + self.displaysBorderOnTracking = displaysBorderOnTracking + self.useLegacyKnob = useLegacyKnob self.isTrackingUpdated = isTrackingUpdated } @@ -114,6 +142,18 @@ public final class SliderComponent: Component { if lhs.knobColor != rhs.knobColor { return false } + if lhs.isEnabled != rhs.isEnabled { + return false + } + if lhs.trackHeight != rhs.trackHeight { + return false + } + if lhs.displaysBorderOnTracking != rhs.displaysBorderOnTracking { + return false + } + if lhs.useLegacyKnob != rhs.useLegacyKnob { + return false + } return true } @@ -123,6 +163,8 @@ public final class SliderComponent: Component { public final class View: UIView { private var nativeSliderView: SliderView? + private let nativeTrackBackgroundView = UIView() + private let nativeTrackForegroundView = UIView() private var sliderView: TGPhotoEditorSliderView? private var component: SliderComponent? @@ -139,6 +181,12 @@ public final class SliderComponent: Component { required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private static let legacyKnobImage: UIImage = generateImage(CGSize(width: 21.0, height: 21.0), rotatedContext: { _, context in + context.setShadow(offset: CGSize(width: 0.0, height: 0.5), blur: 1.5, color: UIColor(white: 0.0, alpha: 0.5).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(x: 2.0, y: 2.0, width: 17.0, height: 17.0)) + })! public func cancelGestures() { if let sliderView = self.sliderView, let gestureRecognizers = sliderView.gestureRecognizers { @@ -158,6 +206,11 @@ public final class SliderComponent: Component { let size = CGSize(width: availableSize.width, height: 44.0) if #available(iOS 26.0, *), component.useNative { + if let sliderView = self.sliderView { + self.sliderView = nil + sliderView.removeFromSuperview() + } + let sliderView: SliderView if let current = self.nativeSliderView { sliderView = current @@ -167,13 +220,15 @@ public final class SliderComponent: Component { sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) sliderView.layer.allowsGroupOpacity = true + self.addSubview(self.nativeTrackBackgroundView) + self.addSubview(self.nativeTrackForegroundView) self.addSubview(sliderView) self.nativeSliderView = sliderView switch component.content { case let .continuous(continuous): - sliderView.minimumValue = Float(continuous.minValue ?? 0.0) - sliderView.maximumValue = 1.0 + sliderView.minimumValue = Float(continuous.minValue ?? continuous.range.lowerBound) + sliderView.maximumValue = Float(continuous.range.upperBound) case let .discrete(discrete): sliderView.minimumValue = 0.0 sliderView.maximumValue = Float(discrete.valueCount - 1) @@ -182,20 +237,69 @@ public final class SliderComponent: Component { } switch component.content { case let .continuous(continuous): + sliderView.minimumValue = Float(continuous.minValue ?? continuous.range.lowerBound) + sliderView.maximumValue = Float(continuous.range.upperBound) sliderView.value = Float(continuous.value) case let .discrete(discrete): + sliderView.minimumValue = 0.0 + sliderView.maximumValue = Float(discrete.valueCount - 1) sliderView.value = Float(discrete.value) } - sliderView.minimumTrackTintColor = component.trackForegroundColor - sliderView.maximumTrackTintColor = component.trackBackgroundColor + + let useCenteredNativeTrack: Bool + if case let .continuous(continuous) = component.content, continuous.range.lowerBound < continuous.startValue && continuous.startValue < continuous.range.upperBound { + useCenteredNativeTrack = true + } else { + useCenteredNativeTrack = false + } + + if useCenteredNativeTrack { + sliderView.minimumTrackTintColor = UIColor.clear + sliderView.maximumTrackTintColor = UIColor.clear + + let trackHeight = component.trackHeight ?? 4.0 + let trackFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - trackHeight) * 0.5)), size: CGSize(width: availableSize.width, height: trackHeight)) + self.nativeTrackBackgroundView.backgroundColor = component.trackBackgroundColor + self.nativeTrackBackgroundView.layer.cornerRadius = trackHeight * 0.5 + self.nativeTrackBackgroundView.alpha = component.isEnabled ? 1.0 : 0.3 + transition.setFrame(view: self.nativeTrackBackgroundView, frame: trackFrame) + + if case let .continuous(continuous) = component.content { + let rangeDistance = max(continuous.range.upperBound - continuous.range.lowerBound, CGFloat.ulpOfOne) + let startPosition = min(max((continuous.startValue - continuous.range.lowerBound) / rangeDistance, 0.0), 1.0) * trackFrame.width + let valuePosition = min(max((continuous.value - continuous.range.lowerBound) / rangeDistance, 0.0), 1.0) * trackFrame.width + let foregroundFrame = CGRect( + origin: CGPoint(x: trackFrame.minX + min(startPosition, valuePosition), y: trackFrame.minY), + size: CGSize(width: abs(valuePosition - startPosition), height: trackFrame.height) + ) + self.nativeTrackForegroundView.backgroundColor = component.trackForegroundColor + self.nativeTrackForegroundView.layer.cornerRadius = trackHeight * 0.5 + self.nativeTrackForegroundView.alpha = component.isEnabled ? 1.0 : 0.3 + transition.setFrame(view: self.nativeTrackForegroundView, frame: foregroundFrame) + } + } else { + sliderView.minimumTrackTintColor = component.trackForegroundColor + sliderView.maximumTrackTintColor = component.trackBackgroundColor + self.nativeTrackBackgroundView.frame = CGRect() + self.nativeTrackForegroundView.frame = CGRect() + } + sliderView.isEnabled = component.isEnabled + sliderView.alpha = component.isEnabled ? 1.0 : 0.3 transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: 44.0))) } else { + self.nativeTrackBackgroundView.frame = CGRect() + self.nativeTrackForegroundView.frame = CGRect() + if let nativeSliderView = self.nativeSliderView { + self.nativeSliderView = nil + nativeSliderView.removeFromSuperview() + } + var internalIsTrackingUpdated: ((Bool) -> Void)? if let isTrackingUpdated = component.isTrackingUpdated { internalIsTrackingUpdated = { [weak self] isTracking in if let self { - if !"".isEmpty { + if component.displaysBorderOnTracking { if isTracking { self.sliderView?.bordered = true } else { @@ -215,51 +319,20 @@ public final class SliderComponent: Component { } else { sliderView = TGPhotoEditorSliderView() sliderView.enablePanHandling = true - if let knobSize = component.knobSize { - sliderView.lineSize = knobSize + 4.0 - } else { - sliderView.lineSize = 4.0 - } - sliderView.trackCornerRadius = sliderView.lineSize * 0.5 sliderView.dotSize = 5.0 - sliderView.minimumValue = 0.0 - sliderView.startValue = 0.0 sliderView.disablesInteractiveTransitionGestureRecognizer = true switch component.content { case let .discrete(discrete): - sliderView.maximumValue = CGFloat(discrete.valueCount - 1) sliderView.positionsCount = discrete.valueCount sliderView.useLinesForPositions = true sliderView.markPositions = discrete.markPositions case .continuous: - sliderView.maximumValue = 1.0 + break } sliderView.backgroundColor = nil sliderView.isOpaque = false - sliderView.backColor = component.trackBackgroundColor - sliderView.startColor = component.trackBackgroundColor - sliderView.trackColor = component.trackForegroundColor - if let knobSize = component.knobSize { - sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) - if let knobColor = component.knobColor { - context.setFillColor(knobColor.cgColor) - } else { - context.setFillColor(UIColor.white.cgColor) - } - context.fillEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - knobSize) * 0.5), y: floor((size.width - knobSize) * 0.5)), size: CGSize(width: knobSize, height: knobSize))) - }) - } else { - sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) - }) - } sliderView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX) @@ -271,22 +344,66 @@ public final class SliderComponent: Component { self.sliderView = sliderView self.addSubview(sliderView) } + if let trackHeight = component.trackHeight { + sliderView.lineSize = trackHeight + } else if let knobSize = component.knobSize { + sliderView.lineSize = knobSize + 4.0 + } else { + sliderView.lineSize = 4.0 + } + sliderView.trackCornerRadius = sliderView.lineSize * 0.5 + sliderView.backColor = component.trackBackgroundColor + sliderView.startColor = component.useLegacyKnob ? UIColor(rgb: 0xffffff) : component.trackBackgroundColor + sliderView.trackColor = component.trackForegroundColor + sliderView.isUserInteractionEnabled = component.isEnabled + sliderView.alpha = component.isEnabled ? (component.useLegacyKnob ? 1.3 : 1.0) : 0.3 + if let knobSize = component.knobSize { + sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + if let knobColor = component.knobColor { + context.setFillColor(knobColor.cgColor) + } else { + context.setFillColor(UIColor.white.cgColor) + } + context.fillEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - knobSize) * 0.5), y: floor((size.width - knobSize) * 0.5)), size: CGSize(width: knobSize, height: knobSize))) + }) + } else if component.useLegacyKnob { + sliderView.knobImage = View.legacyKnobImage + } else { + sliderView.knobImage = generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setShadow(offset: CGSize(width: 0.0, height: -3.0), blur: 12.0, color: UIColor(white: 0.0, alpha: 0.25).cgColor) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0))) + }) + } sliderView.lowerBoundTrackColor = component.minTrackForegroundColor switch component.content { case let .discrete(discrete): - sliderView.value = CGFloat(discrete.value) + sliderView.minimumValue = 0.0 + sliderView.maximumValue = CGFloat(discrete.valueCount - 1) + sliderView.startValue = 0.0 + sliderView.positionsCount = discrete.valueCount + sliderView.useLinesForPositions = true + sliderView.markPositions = discrete.markPositions if let minValue = discrete.minValue { sliderView.lowerBoundValue = CGFloat(minValue) } else { sliderView.lowerBoundValue = 0.0 } + sliderView.value = CGFloat(discrete.value) case let .continuous(continuous): - sliderView.value = continuous.value + sliderView.minimumValue = continuous.range.lowerBound + sliderView.maximumValue = continuous.range.upperBound + sliderView.startValue = continuous.startValue + sliderView.positionsCount = 0 if let minValue = continuous.minValue { sliderView.lowerBoundValue = minValue } else { sliderView.lowerBoundValue = 0.0 } + sliderView.value = continuous.value } sliderView.interactionBegan = { internalIsTrackingUpdated?(true) diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 57d7623522..b494ea0425 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -1033,10 +1033,8 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { completionImpl?(stars) } ), navigationBarAppearance: .default, presentationMode: .modal, theme: customTheme.flatMap { .custom($0) } ?? .default) - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) + + let cancelItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.setLeftBarButton(cancelItem, animated: false) self.navigationPresentation = .modal 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/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 626fc02427..463115e109 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -3323,6 +3323,7 @@ public final class StorageUsageScreen: ViewControllerComponentContainer { if peer != nil { self.navigationPresentation = .modal + self._hasGlassStyle = true } self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true))) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 3dfa33e15c..b73ff9df5a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -36,7 +36,6 @@ swift_library( "//submodules/PhoneNumberFormat", "//submodules/TelegramIntents", "//submodules/LegacyUI", - "//submodules/WebSearchUI", "//submodules/PremiumUI", "//submodules/ICloudResources", "//submodules/LegacyComponents", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index a09afd7f9d..8e9b7278b6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -4565,7 +4565,7 @@ public final class StoryItemSetContainerComponent: Component { } switch action { case let .url(url, concealed): - let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak self] c in + let _ = component.context.sharedContext.openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, webpage: nil, concealed: concealed, forceConcealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak self] c in guard let self, let component = self.component, let controller = component.controller() else { return } @@ -4575,13 +4575,13 @@ public final class StoryItemSetContainerComponent: Component { return } self.sendMessageContext.openResolved(view: self, result: resolved, forceExternal: false, concealed: concealed) - }, alertDisplayUpdated: { [weak self] alertController in + }, progress: nil, alertDisplayUpdated: { [weak self] alertController in guard let self else { return } self.sendMessageContext.statusController = alertController self.updateIsProgressPaused() - }) + }, concealedAlertOption: nil) case let .textMention(value): self.sendMessageContext.openPeerMention(view: self, name: value) case let .peerMention(peerId, _): @@ -4599,7 +4599,7 @@ public final class StoryItemSetContainerComponent: Component { return } self.sendMessageContext.presentTextEntityActions(view: self, action: action, openUrl: { [weak self] url, concealed in - let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, concealed: concealed, skipUrlAuth: false, skipConcealedAlert: false, present: { [weak self] c in + let _ = component.context.sharedContext.openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, webpage: nil, concealed: concealed, forceConcealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: false, present: { [weak self] c in guard let self, let component = self.component, let controller = component.controller() else { return } @@ -4609,7 +4609,7 @@ public final class StoryItemSetContainerComponent: Component { return } self.sendMessageContext.openResolved(view: self, result: resolved, forceExternal: false, concealed: concealed) - }) + }, progress: nil, alertDisplayUpdated: nil, concealedAlertOption: nil) }) }, textSelectionAction: { [weak self] text, action in diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index bcebfb8be8..7b342f3c00 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -17,7 +17,6 @@ import TextFormat import PhoneNumberFormat import TelegramIntents import LegacyUI -import WebSearchUI import ChatTimerScreen import PremiumUI import ICloudResources @@ -1848,6 +1847,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let attachmentController = AttachmentController( context: component.context, updatedPresentationData: updatedPresentationData, + style: .glass, chatLocation: .peer(id: peer.id), buttons: buttons, initialButton: initialButton @@ -1981,7 +1981,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != component.context.account.peerId let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let controller = LocationPickerController(context: component.context, updatedPresentationData: updatedPresentationData, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self, weak view] location, _, _, _, _ in + let controller = LocationPickerController(context: component.context, style: .glass, updatedPresentationData: updatedPresentationData, mode: .share(peer: peer, selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self, weak view] location, _, _, _, _ in guard let self, let view else { return } @@ -2160,58 +2160,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { })) return true case .gift: - /*let premiumGiftOptions = strongSelf.presentationInterfaceState.premiumGiftOptions - if !premiumGiftOptions.isEmpty { - let controller = PremiumGiftScreen(context: context, peerId: peer.id, options: premiumGiftOptions, source: .attachMenu, pushController: { [weak self] c in - if let strongSelf = self { - strongSelf.push(c) - } - }, completion: { [weak self] in - if let strongSelf = self { - strongSelf.hintPlayNextOutgoingGift() - strongSelf.attachmentController?.dismiss(animated: true) - } - }) - completion(controller, controller.mediaPickerContext) - strongSelf.controllerNavigationDisposable.set(nil) - - let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).start() - }*/ - //TODO:gift controller break case let .app(bot): let _ = bot -// let params = WebAppParameters(source: .attachMenu, peerId: peer.id, botId: bot.peer.id, botName: bot.shortName, botVerified: bot.peer.isVerified, url: nil, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, forceHasSettings: false, fullSize: true) -// let theme = component.theme -// let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) -// let controller = WebAppController(context: component.context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil, threadId: nil) -// controller.openUrl = { [weak self] url, _, _, _ in -// guard let self else { -// return -// } -// let _ = self -// //self?.openUrl(url, concealed: true, forceExternal: true) -// } -// controller.getNavigationController = { [weak view] in -// guard let view, let controller = view.component?.controller() else { -// return nil -// } -// return controller.navigationController as? NavigationController -// } -// controller.completion = { [weak self] in -// guard let self else { -// return -// } -// let _ = self -// /*if let strongSelf = self { -// strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { -// $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil) } -// }) -// strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() -// }*/ -// } -// completion(controller, controller.mediaPickerContext) -// self.controllerNavigationDisposable.set(nil) default: break } @@ -2255,7 +2206,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } let theme = component.theme - let controller = MediaPickerScreenImpl(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), peer: peer, threadTitle: nil, chatLocation: .peer(id: peer.id), bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) + let controller = MediaPickerScreenImpl(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), style: .glass, peer: peer, threadTitle: nil, chatLocation: .peer(id: peer.id), bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, subject: subject, saveEditedPhotos: saveEditedPhotos) let mediaPickerContext = controller.mediaPickerContext controller.openCamera = { [weak self, weak view] cameraView in guard let self, let view else { @@ -2265,29 +2216,6 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.openCamera(view: view, peer: peer, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, cameraView: cameraView) } } - controller.presentWebSearch = { [weak self, weak view, weak controller] mediaGroups, activateOnDisplay in - guard let self, let view, let controller else { - return - } - self.presentWebSearch(view: view, activateOnDisplay: activateOnDisplay, present: { [weak controller] c, a in - controller?.present(c, in: .current) - if let webSearchController = c as? WebSearchController { - webSearchController.searchingUpdated = { [weak mediaGroups] searching in - if let mediaGroups = mediaGroups, mediaGroups.isNodeLoaded { - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - transition.updateAlpha(node: mediaGroups.displayNode, alpha: searching ? 0.0 : 1.0) - mediaGroups.displayNode.isUserInteractionEnabled = !searching - } - } - webSearchController.present(mediaGroups, in: .current) - webSearchController.dismissed = { - updateMediaPickerContext(mediaPickerContext) - } - controller?.webSearchController = webSearchController - updateMediaPickerContext(webSearchController.mediaPickerContext) - } - }) - } controller.presentSchedulePicker = { [weak self, weak view] media, done in guard let self, let view else { return @@ -2368,40 +2296,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] - configureLegacyAssetPicker(controller, context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak view, weak legacyController] in - if let view, let component = view.component { - let theme = component.theme - let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let controller = WebSearchController(context: component.context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { [weak view] results, selectionState, editingState, silentPosting, scheduleTime in - if let legacyController = legacyController { - legacyController.dismiss() - } - guard let view else { - return - } - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak view] result in - if let strongSelf = self, let view { - strongSelf.enqueueChatContextResult(view: view, peer: peer, replyMessageId: replyMessageId, storyId: replyToStoryId, results: results, result: result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }, enqueueMediaMessages: { [weak view] signals in - if let strongSelf = self, let view { - if editingMedia { - strongSelf.editMessageMediaWithLegacySignals(view: view, signals: signals) - } else { - strongSelf.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - } - }) - })) - controller.getCaptionPanelView = { [weak view] in - guard let self, let view else { - return nil - } - return self.getCaptionPanelView(view: view, peer: peer) - } - component.controller()?.push(controller) - } - }, presentSelectionLimitExceeded: { [weak view] in + configureLegacyAssetPicker(controller, context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: nil, presentSelectionLimitExceeded: { [weak view] in guard let view, let component = view.component else { return } @@ -2671,44 +2566,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { sendMessage(scheduleTime) } - - private func presentWebSearch(view: StoryItemSetContainerComponent.View, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { - guard let component = view.component else { - return - } - let context = component.context - let peer = component.slice.effectivePeer - let storyId = component.slice.item.storyItem.id - - let theme = component.theme - let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - |> deliverOnMainQueue).start(next: { [weak self, weak view] configuration in - if let self { - let controller = WebSearchController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: configuration, mode: .media(attachment: true, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self, weak view] result in - if let self, let view { - self.performSendContextResultAction(view: view, results: results, result: result, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }, enqueueMediaMessages: { [weak self, weak view] signals in - if let self, let view, !signals.isEmpty { - self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: EngineStoryId(peerId: peer.id, id: storyId), signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }) - }), activateOnDisplay: activateOnDisplay) - controller.getCaptionPanelView = { [weak self, weak view] in - if let view { - return self?.getCaptionPanelView(view: view, peer: peer) - } else { - return nil - } - } - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - }) - } - + private func getCaptionPanelView(view: StoryItemSetContainerComponent.View, peer: EnginePeer, mediaPicker: MediaPickerScreenImpl? = nil) -> TGCaptionPanelView? { guard let component = view.component else { return nil @@ -2848,6 +2706,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return nil } return self.getCaptionPanelView(view: view, peer: peer) + }, photoToolbarView: { [context = component.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }, dismissedWithResult: { [weak self] in guard let self else { return @@ -3429,12 +3289,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case let .url(url, concealed): if canOpenIn { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = OpenInActionSheetController(context: component.context, item: .url(url: url), openUrl: { url in + let actionSheet = OpenInOptionsScreen(context: component.context, item: .url(url: url), openUrl: { url in if let navigationController = component.controller()?.navigationController as? NavigationController { component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } }) - component.controller()?.present(actionSheet, in: .window(.root)) + component.controller()?.push(actionSheet) } else { openUrl(url, concealed) } @@ -3825,20 +3685,20 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return case let .link(_, url): let action = { - let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, concealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak controller] c in + let _ = component.context.sharedContext.openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, webpage: nil, concealed: false, forceConcealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak controller] c in controller?.present(c, in: .window(.root)) }, openResolved: { [weak self, weak view] resolved in guard let self, let view else { return } self.openResolved(view: view, result: resolved, forceExternal: false, concealed: false) - }, alertDisplayUpdated: { [weak self, weak view] alertController in + }, progress: nil, alertDisplayUpdated: { [weak self, weak view] alertController in guard let self, let view else { return } self.statusController = alertController view.updateIsProgressPaused() - }) + }, concealedAlertOption: nil) } if immediate { action() @@ -3852,20 +3712,20 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case let .starGift(_, slug): useGesturePosition = true let action = { - let _ = openUserGeneratedUrl(context: component.context, peerId: nil, url: "https://t.me/nft/\(slug)", concealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak controller] c in + let _ = component.context.sharedContext.openUserGeneratedUrl(context: component.context, peerId: nil, url: "https://t.me/nft/\(slug)", webpage: nil, concealed: false, forceConcealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak controller] c in controller?.present(c, in: .window(.root)) }, openResolved: { [weak self, weak view] resolved in guard let self, let view else { return } self.openResolved(view: view, result: resolved, forceExternal: false, concealed: false) - }, alertDisplayUpdated: { [weak self, weak view] alertController in + }, progress: nil, alertDisplayUpdated: { [weak self, weak view] alertController in guard let self, let view else { return } self.statusController = alertController view.updateIsProgressPaused() - }) + }, concealedAlertOption: nil) } if immediate { action() diff --git a/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/BUILD b/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/BUILD index 72a004e510..233f4b8e1a 100644 --- a/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/BUILD +++ b/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/BUILD @@ -17,7 +17,6 @@ swift_library( "//submodules/InstantPageUI", "//submodules/AccountContext", "//submodules/LocalMediaResources", - "//submodules/WebSearchUI", "//submodules/InstantPageCache", "//submodules/SettingsUI", "//submodules/WallpaperResources", diff --git a/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/Sources/TelegramUIDeclareEncodables.swift b/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/Sources/TelegramUIDeclareEncodables.swift index 203fcd6656..9e0b0cb81f 100644 --- a/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/Sources/TelegramUIDeclareEncodables.swift +++ b/submodules/TelegramUI/Components/TelegramUIDeclareEncodables/Sources/TelegramUIDeclareEncodables.swift @@ -5,7 +5,6 @@ import TelegramNotices import InstantPageUI import AccountContext import LocalMediaResources -import WebSearchUI import InstantPageCache import SettingsUI import WallpaperResources diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 5bb7677ff5..591b0f708c 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -1035,7 +1035,7 @@ public final class TextFieldComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: component.theme) let updatedPresentationData: (initial: PresentationData, signal: Signal) = (presentationData, .single(presentationData)) - let controller = component.context.sharedContext.makeLinkEditController(context: component.context, updatedPresentationData: updatedPresentationData, text: text.string, link: link, apply: { [weak self] link in + let controller = component.context.sharedContext.makeLinkEditController(context: component.context, updatedPresentationData: updatedPresentationData, text: text.string, link: link, apply: { [weak self] link, _ in if let self { if let link { if !link.isEmpty { 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/TokenListTextField/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift index 03e2207b99..4b8f594141 100644 --- a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift @@ -304,6 +304,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.scrollNode = ASScrollNode() self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.view.scrollsToTop = false self.placeholderNode = ASTextNode() self.placeholderNode.isUserInteractionEnabled = false @@ -312,6 +313,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.placeholderTextColor) self.textFieldScrollNode = ASScrollNode() + self.textFieldScrollNode.view.scrollsToTop = false self.textFieldNode = TextFieldNode() self.textFieldNode.textField.font = Font.regular(15.0) diff --git a/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift b/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift index 331f8b6f05..3dc429d3ac 100644 --- a/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift +++ b/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift @@ -260,6 +260,7 @@ public final class TranslationLanguagesContextMenuContent: ContextControllerItem self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.scrollNode.clipsToBounds = false + self.scrollNode.view.scrollsToTop = false super.init() 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/PeopleNearby.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Link.imageset/Contents.json similarity index 73% rename from submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/PeopleNearby.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Link.imageset/Contents.json index 6b7b65b4e5..c2b74e56fe 100644 --- a/submodules/TelegramUI/Images.xcassets/Contact List/Context Menu/PeopleNearby.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Link.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "peoplenearbyon_24.pdf", + "filename" : "linktab_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Link.imageset/linktab_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Link.imageset/linktab_30.pdf new file mode 100644 index 0000000000..acf61dd694 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Attach Menu/Link.imageset/linktab_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/BackArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Globe.imageset/Contents.json similarity index 77% rename from submodules/TelegramUI/Images.xcassets/Navigation/BackArrow.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Globe.imageset/Contents.json index 000c70dfaf..ceda3a93f4 100644 --- a/submodules/TelegramUI/Images.xcassets/Navigation/BackArrow.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Globe.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "BackArrow.svg", + "filename" : "globe (1).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Globe.imageset/globe (1).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Globe.imageset/globe (1).pdf new file mode 100644 index 0000000000..152dee90c7 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Globe.imageset/globe (1).pdf differ 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/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/Images.xcassets/Location/OptionGlobeAmerica.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/Contents.json new file mode 100644 index 0000000000..d4f692b39a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc8.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/loc8.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/loc8.pdf new file mode 100644 index 0000000000..c701eec262 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAmerica.imageset/loc8.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/Contents.json new file mode 100644 index 0000000000..b96e295d1a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc9.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/loc9.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/loc9.pdf new file mode 100644 index 0000000000..12da3dbe05 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeAsia.imageset/loc9.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/Contents.json new file mode 100644 index 0000000000..4b76f1f60f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc7.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/loc7.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/loc7.pdf new file mode 100644 index 0000000000..e511bf1e9a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/OptionGlobeEurope.imageset/loc7.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionHybrid.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionHybrid.imageset/Contents.json new file mode 100644 index 0000000000..47751e58bd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionHybrid.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc11.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionHybrid.imageset/loc11.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionHybrid.imageset/loc11.pdf new file mode 100644 index 0000000000..43ec75dc46 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/OptionHybrid.imageset/loc11.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/Contents.json index 2468a09a36..0ddc25e04e 100644 --- a/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "headerlocation_30.pdf", + "filename" : "loc1.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/loc1.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/loc1.pdf new file mode 100644 index 0000000000..df05f9d099 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/loc1.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/Contents.json new file mode 100644 index 0000000000..2836db899e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/headermap_30.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/loc2.pdf similarity index 59% rename from submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/headermap_30.pdf rename to submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/loc2.pdf index 070e541975..788817e753 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/headermap_30.pdf and b/submodules/TelegramUI/Images.xcassets/Location/OptionLocating.imageset/loc2.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/Contents.json index b67dccfbe4..8980d96ec7 100644 --- a/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "headermap_30.pdf", + "filename" : "loc5.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/loc5.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/loc5.pdf new file mode 100644 index 0000000000..4d604fb8c0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/OptionMap.imageset/loc5.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/Contents.json new file mode 100644 index 0000000000..a347dfb45c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "loc4.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/loc4.pdf b/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/loc4.pdf new file mode 100644 index 0000000000..e06bf01f7a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/OptionTracking.imageset/loc4.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/Contents.json similarity index 83% rename from submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/Contents.json index cb8e30c043..b3a12bd66e 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "tools_30.pdf", + "filename" : "tools_30 (4).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/tools_30 (4).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/tools_30 (4).pdf new file mode 100644 index 0000000000..6a93dd33d3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Adjustments.imageset/tools_30 (4).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Crop.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Crop.imageset/Contents.json new file mode 100644 index 0000000000..a75bdb1b8d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Crop.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "crop_30 (4).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Crop.imageset/crop_30 (4).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Crop.imageset/crop_30 (4).pdf new file mode 100644 index 0000000000..c55d00ce13 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Crop.imageset/crop_30 (4).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json index 6074e4a7f0..912db07593 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "pencil_30.pdf", + "filename" : "pencil_30 (4).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30 (4).pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30 (4).pdf new file mode 100644 index 0000000000..211becd4a7 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30 (4).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf deleted file mode 100644 index 20a9307e93..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Pencil.imageset/pencil_30.pdf +++ /dev/null @@ -1,93 +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 4.170021 4.169979 cm -1.000000 1.000000 1.000000 scn -1.660000 10.830000 m -1.660000 15.894451 5.765549 20.000000 10.830000 20.000000 c -15.894451 20.000000 20.000000 15.894451 20.000000 10.830000 c -20.000000 7.821688 18.551388 5.151716 16.313671 3.479595 c -14.640235 11.010053 l -14.555845 11.389809 14.219020 11.660000 13.830000 11.660000 c -13.553333 11.660000 l -11.778684 16.983950 l -11.474785 17.895645 10.185216 17.895649 9.881317 16.983952 c -8.106667 11.660000 l -7.830000 11.660000 l -7.440980 11.660000 7.104155 11.389809 7.019765 11.010053 c -5.346330 3.479595 l -3.108612 5.151716 1.660000 7.821688 1.660000 10.830000 c -h -13.164198 10.000000 m -14.815517 2.569059 l -13.610337 1.986547 12.258304 1.660000 10.830000 1.660000 c -9.401696 1.660000 8.049662 1.986547 6.844482 2.569059 c -8.495803 10.000000 l -13.164198 10.000000 l -h -10.830000 21.660000 m -4.848756 21.660000 0.000000 16.811243 0.000000 10.830000 c -0.000000 4.848755 4.848756 0.000000 10.830000 0.000000 c -16.811245 0.000000 21.660000 4.848755 21.660000 10.830000 c -21.660000 16.811243 16.811245 21.660000 10.830000 21.660000 c -h -f* -n -Q - -endstream -endobj - -3 0 obj - 1162 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 30.000000 30.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 -0000001252 00000 n -0000001275 00000 n -0000001448 00000 n -0000001522 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -1581 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json deleted file mode 100644 index 71cf2c0e13..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "undo.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png deleted file mode 100644 index 1dc7913d2b..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Media Editor/Redo.imageset/undo.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf deleted file mode 100644 index 25301318c7..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Tools.imageset/tools_30.pdf +++ /dev/null @@ -1,101 +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 4.170021 4.169979 cm -1.000000 1.000000 1.000000 scn -5.830000 21.660000 m -6.288396 21.660000 6.660000 21.288397 6.660000 20.830000 c -6.660000 17.660000 l -20.830000 17.660000 l -21.288397 17.660000 21.660000 17.288397 21.660000 16.830000 c -21.660000 16.371603 21.288397 16.000000 20.830000 16.000000 c -6.660000 16.000000 l -6.660000 12.830000 l -6.660000 12.371604 6.288396 12.000000 5.830000 12.000000 c -5.371603 12.000000 5.000000 12.371604 5.000000 12.830000 c -5.000000 16.000000 l -0.830000 16.000000 l -0.371603 16.000000 0.000000 16.371603 0.000000 16.830000 c -0.000000 17.288397 0.371603 17.660000 0.830000 17.660000 c -5.000000 17.660000 l -5.000000 20.830000 l -5.000000 21.288397 5.371603 21.660000 5.830000 21.660000 c -h -20.830000 3.999998 m -21.288397 3.999998 21.660000 4.371601 21.660000 4.829998 c -21.660000 5.288395 21.288397 5.659998 20.830000 5.659998 c -16.660000 5.659998 l -16.660000 8.830000 l -16.660000 9.288396 16.288397 9.660000 15.830000 9.660000 c -15.371604 9.660000 15.000000 9.288396 15.000000 8.830000 c -15.000000 5.659998 l -0.830000 5.659998 l -0.371603 5.659998 0.000000 5.288395 0.000000 4.829998 c -0.000000 4.371601 0.371603 3.999998 0.830000 3.999998 c -15.000000 3.999998 l -15.000000 0.829998 l -15.000000 0.371603 15.371604 0.000000 15.830000 0.000000 c -16.288397 0.000000 16.660000 0.371603 16.660000 0.829998 c -16.660000 3.999998 l -20.830000 3.999998 l -h -f* -n -Q - -endstream -endobj - -3 0 obj - 1452 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 30.000000 30.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 -0000001542 00000 n -0000001565 00000 n -0000001738 00000 n -0000001812 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -1871 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json index 71cf2c0e13..7a253e3f25 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "undo.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "undo_30 (4).pdf", + "idiom" : "universal" } ], "info" : { diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png deleted file mode 100644 index c2b1063a39..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/headerlocation_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo_30 (4).pdf similarity index 66% rename from submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/headerlocation_30.pdf rename to submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo_30 (4).pdf index 3ed9097bd4..deba9dc00c 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Location/OptionLocate.imageset/headerlocation_30.pdf and b/submodules/TelegramUI/Images.xcassets/Media Editor/Undo.imageset/undo_30 (4).pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/BackArrow.imageset/BackArrow.svg b/submodules/TelegramUI/Images.xcassets/Navigation/BackArrow.imageset/BackArrow.svg deleted file mode 100644 index 46caf41fd7..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Navigation/BackArrow.imageset/BackArrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/200x200bb-100.png b/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/200x200bb-100.png new file mode 100644 index 0000000000..baf0736c4b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/200x200bb-100.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Contents.json index be50f3de3e..78579e21eb 100644 --- a/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Contents.json @@ -6,17 +6,16 @@ }, { "idiom" : "universal", - "filename" : "Safari@2x.png", "scale" : "2x" }, { + "filename" : "200x200bb-100.png", "idiom" : "universal", - "filename" : "Safari@3x.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@2x.png b/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@2x.png deleted file mode 100644 index a833be5d05..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@3x.png b/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@3x.png deleted file mode 100644 index 9de9152228..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Open In/Safari.imageset/Safari@3x.png 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 { @@ -3780,7 +3810,7 @@ extension ChatControllerImpl { } } - let controller = chatTextLinkEditController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, text: text?.string ?? "", link: link, apply: { [weak self] link in + let controller = chatTextLinkEditController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, text: strongSelf.presentationData.strings.TextFormat_AddLinkText(text?.string ?? "").string, link: link, apply: { [weak self] link, _ in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link { if !link.isEmpty { @@ -3909,12 +3939,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/ChatControllerOpenLinkContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift index 0b6afc013c..532a7b7719 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift @@ -7,29 +7,217 @@ import Display import ContextUI import UndoUI import AccountContext +import BrowserUI import ChatMessageItemView import ChatMessageItemCommon import MessageUI import ChatControllerInteraction +import TelegramUIPreferences +import UrlEscaping import UrlWhitelist import OpenInExternalAppUI import SafariServices +private struct ChatLinkOpenMode { + let shouldOpenInApp: Bool +} + +private enum ChatLinkReverseOpenTarget { + case inApp + case externalBrowser + + var checkboxTitle: String { + switch self { + case .inApp: + return "Always open this site in-app" + case .externalBrowser: + return "Always open this site in browser" + } + } + + var openExternalBrowser: Bool { + switch self { + case .inApp: + return false + case .externalBrowser: + return true + } + } +} + +private func chatLinkContextMenuCanonicalUrl(from url: String) -> URL? { + var urlWithScheme = url + if !url.contains("://") && !url.hasPrefix("mailto:") { + urlWithScheme = "http://" + url + } + if let parsed = URL(string: urlWithScheme) { + return parsed + } else if let encoded = (urlWithScheme as NSString).addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) { + return URL(string: encoded) + } + return nil +} + +private func chatLinkContextMenuOpenMode(context: AccountContext, url: String) -> Signal { + let lowercasedUrl = url.lowercased() + if lowercasedUrl.hasPrefix("mailto:") || lowercasedUrl.hasPrefix("tel:") || lowercasedUrl.hasPrefix("calshow:") { + return .single(nil) + } + + guard let parsedUrl = chatLinkContextMenuCanonicalUrl(from: url) else { + return .single(nil) + } + + let scheme = (parsedUrl.scheme ?? "").lowercased() + guard scheme == "http" || scheme == "https" || scheme == "tonsite" else { + return .single(nil) + } + + let host = parsedUrl.host?.lowercased() ?? "" + if host.isEmpty { + return .single(nil) + } + if host == "t.me" || host == "telegram.me" || host == "telegram.dog" { + return .single(nil) + } + if host.hasSuffix(".ton") || scheme.hasPrefix("tonsite") { + return .single(nil) + } + + return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.webBrowserSettings)) + |> take(1) + |> map { accountSettingsEntry -> ChatLinkOpenMode? in + let accountSettings = accountSettingsEntry?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + let normalizedHost = ".\(host)" + let exceptions = accountSettings.openExternalBrowser ? accountSettings.inAppExceptions : accountSettings.externalExceptions + var isExceptedDomain = false + for exception in exceptions { + if normalizedHost.hasSuffix(".\(exception.domain.lowercased())") { + isExceptedDomain = true + break + } + } + + let shouldOpenInApp: Bool + if accountSettings.openExternalBrowser { + shouldOpenInApp = isExceptedDomain + } else { + shouldOpenInApp = !isExceptedDomain + } + return ChatLinkOpenMode(shouldOpenInApp: shouldOpenInApp) + } +} + extension ChatControllerImpl { + private func presentOpenLinkConfirmation(_ url: String, target: ChatLinkReverseOpenTarget) { + var exceptionAdded = false + let disposable = self.context.sharedContext.openUserGeneratedUrl( + context: self.context, + peerId: self.contentData?.state.peerView?.peerId, + url: url, + webpage: nil, + concealed: false, + forceConcealed: true, + skipUrlAuth: false, + skipConcealedAlert: false, + forceDark: false, + present: { [weak self] c in + self?.present(c, in: .window(.root)) + }, + openResolved: { [weak self] result in + guard let self else { + return + } + switch target { + case .inApp: + if case let .externalUrl(resolvedUrl) = result, let navigationController = self.effectiveNavigationController { + self.chatDisplayNode.dismissInput() + let controller = BrowserScreen(context: self.context, subject: .webPage(url: resolvedUrl)) + navigationController.pushViewController(controller) + + if exceptionAdded { + Queue.mainQueue().after(0.5) { + let tooltipScreen = UndoOverlayController( + presentationData: self.presentationData, + content: .actionSucceeded(title: "Exception Added", text: "This site will always open in-app.", cancel: "", destructive: false), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in + return false + } + ) + controller.present(tooltipScreen, in: .current) + } + } + } else { + self.openResolved(result: result, sourceMessageId: nil, forceExternal: false, concealed: true) + } + case .externalBrowser: + if case .externalUrl = result { + let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]) + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] sharedData in + guard let self else { + return + } + self.chatDisplayNode.dismissInput() + + let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings + var defaultWebBrowser = settings.defaultWebBrowser + if defaultWebBrowser == nil || defaultWebBrowser == "inApp" || defaultWebBrowser == "inAppSafari" { + defaultWebBrowser = "safari" + } + + let targetUrl = chatLinkContextMenuCanonicalUrl(from: url)?.absoluteString ?? url + let openInOptions = availableOpenInOptions(context: self.context, item: .url(url: targetUrl)) + if let option = openInOptions.first(where: { $0.identifier == defaultWebBrowser }) { + if case let .openUrl(openInUrl) = option.action() { + self.context.sharedContext.applicationBindings.openUrl(openInUrl) + } else { + self.context.sharedContext.applicationBindings.openUrl(targetUrl) + } + } else { + self.context.sharedContext.applicationBindings.openUrl(targetUrl) + } + }) + } else { + self.openResolved(result: result, sourceMessageId: nil, forceExternal: true, concealed: false) + } + } + }, + progress: nil, + alertDisplayUpdated: nil, + concealedAlertOption: OpenUserGeneratedUrlConcealedAlertOption(title: target.checkboxTitle, action: { [weak self] in + guard let self else { + return + } + let _ = toggleWebBrowserSettingsException( + postbox: self.context.account.postbox, + network: self.context.account.network, + openExternalBrowser: target.openExternalBrowser, + delete: false, + url: url + ).startStandalone() + + exceptionAdded = true + }) + ) + self.navigationActionDisposable.set(disposable) + } + func openLinkContextMenu(url: String, params: ChatControllerInteraction.LongTapParams) -> Void { guard let message = params.message, let contentNode = params.contentNode else { var (cleanUrl, _) = parseUrl(url: url, wasConcealed: false) var canAddToReadingList = true - var canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url)).count > 1 let mailtoString = "mailto:" let telString = "tel:" var openText = self.presentationData.strings.Conversation_LinkDialogOpen var phoneNumber: String? - + var isPhoneNumber = false var isEmail = false var hasOpenAction = true - + if cleanUrl.hasPrefix(mailtoString) { canAddToReadingList = false cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) @@ -39,81 +227,95 @@ extension ChatControllerImpl { phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) cleanUrl = phoneNumber! openText = self.presentationData.strings.UserInfo_PhoneCall - canOpenIn = false isPhoneNumber = true - + if cleanUrl.hasPrefix("+888") { hasOpenAction = false } - } else if canOpenIn { - openText = self.presentationData.strings.Conversation_FileOpenIn } - - let actionSheet = ActionSheetController(presentationData: self.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: cleanUrl)) - if hasOpenAction { - items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - if canOpenIn { - strongSelf.openUrlIn(url) - } else { - strongSelf.openUrl(url, concealed: false) - } - } - })) - } - if let phoneNumber = phoneNumber { - items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.controllerInteraction?.addContact(phoneNumber) - } - })) - } - items.append(ActionSheetButtonItem(title: canAddToReadingList ? self.presentationData.strings.ShareMenu_CopyShareLink : self.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() + + let _ = (chatLinkContextMenuOpenMode(context: self.context, url: url) + |> deliverOnMainQueue).startStandalone(next: { [weak self] openMode in guard let self else { return } - UIPasteboard.general.string = cleanUrl - - let content: UndoOverlayContent - if isPhoneNumber { - content = .copy(text: self.presentationData.strings.Conversation_PhoneCopied) - } else if isEmail { - content = .copy(text: self.presentationData.strings.Conversation_EmailCopied) - } else if canAddToReadingList { - content = .linkCopied(title: nil, text: self.presentationData.strings.Conversation_LinkCopied) - } else { - content = .copy(text: self.presentationData.strings.Conversation_TextCopied) - } - self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - if canAddToReadingList { - items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let link = URL(string: url) { - let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + + let actionSheet = ActionSheetController(presentationData: self.presentationData) + var items: [ActionSheetItem] = [] + items.append(ActionSheetTextItem(title: cleanUrl)) + if hasOpenAction { + items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + self?.openUrl(url, concealed: false) + })) + + if let openMode { + //TODO:localize + let reverseText = openMode.shouldOpenInApp ? "Open in Browser" : "Open In-App" + items.append(ActionSheetButtonItem(title: reverseText, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + guard let self else { + return + } + if openMode.shouldOpenInApp { + self.presentOpenLinkConfirmation(url, target: .externalBrowser) + } else { + self.presentOpenLinkConfirmation(url, target: .inApp) + } + })) } - })) - } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + } + if let phoneNumber = phoneNumber { + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.controllerInteraction?.addContact(phoneNumber) + } + })) + } + items.append(ActionSheetButtonItem(title: canAddToReadingList ? self.presentationData.strings.ShareMenu_CopyShareLink : self.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet, weak self] in actionSheet?.dismissAnimated() - }) - ])]) - self.chatDisplayNode.dismissInput() - self.present(actionSheet, in: .window(.root)) - + guard let self else { + return + } + UIPasteboard.general.string = cleanUrl + + let content: UndoOverlayContent + if isPhoneNumber { + content = .copy(text: self.presentationData.strings.Conversation_PhoneCopied) + } else if isEmail { + content = .copy(text: self.presentationData.strings.Conversation_EmailCopied) + } else if canAddToReadingList { + content = .linkCopied(title: nil, text: self.presentationData.strings.Conversation_LinkCopied) + } else { + content = .copy(text: self.presentationData.strings.Conversation_TextCopied) + } + self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + if canAddToReadingList { + items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.chatDisplayNode.dismissInput() + self.present(actionSheet, in: .window(.root)) + }) + return } - + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { return } - + var updatedMessages = messages for i in 0 ..< updatedMessages.count { if updatedMessages[i].id == message.id { @@ -122,80 +324,100 @@ extension ChatControllerImpl { break } } - + var (cleanUrl, _) = parseUrl(url: url, wasConcealed: false) var canAddToReadingList = true - let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url)).count > 1 - + var isEmail = false let mailtoString = "mailto:" - var openText = self.presentationData.strings.Conversation_LinkDialogOpen + let openText = self.presentationData.strings.Conversation_LinkDialogOpen var copyText = self.presentationData.strings.Conversation_ContextMenuCopyLink if cleanUrl.hasPrefix(mailtoString) { canAddToReadingList = false cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) copyText = self.presentationData.strings.Conversation_ContextMenuCopyEmail isEmail = true - } else if canOpenIn { - openText = self.presentationData.strings.Conversation_FileOpenIn } - + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture let gesture: ContextGesture? = nil - + let source: ContextContentSource = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) - - var items: [ContextMenuItem] = [] - - items.append( - .action(ContextMenuActionItem(text: openText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in - guard let self else { - return - } - f(.default) - - if canOpenIn { - self.openUrlIn(url) - } - else { - self.openUrl(url, concealed: false) - } - })) - ) - - items.append( - .action(ContextMenuActionItem(text: copyText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in - f(.default) - guard let self else { - return - } - - UIPasteboard.general.string = cleanUrl + let itemsSignal = chatLinkContextMenuOpenMode(context: self.context, url: url) + |> deliverOnMainQueue + |> map { [weak self] openMode -> ContextController.Items in + guard let self else { + return ContextController.Items(content: .list([])) + } + + var items: [ContextMenuItem] = [] - self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: isEmail ? presentationData.strings.Conversation_EmailCopied : presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - ) - - if canAddToReadingList { items.append( - .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_AddToReadingList, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: theme.contextMenu.primaryColor) }, action: { _, f in + .action(ContextMenuActionItem(text: openText, icon: { theme in return generateTintedImage(image: openMode?.shouldOpenInApp == true ? UIImage(bundleImageName: "Chat/Context Menu/Browser") : UIImage(bundleImageName: "Chat/Context Menu/Globe"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) - - if let link = URL(string: url) { - let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + + guard let self else { + return } + self.openUrl(url, concealed: false) })) ) + + if let openMode { + let reverseText = openMode.shouldOpenInApp ? "Open in Browser" : "Open In-App" + items.append( + .action(ContextMenuActionItem(text: reverseText, icon: { theme in return generateTintedImage(image: openMode.shouldOpenInApp ? UIImage(bundleImageName: "Chat/Context Menu/Globe") : UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + if openMode.shouldOpenInApp { + self.presentOpenLinkConfirmation(url, target: .externalBrowser) + } else { + self.presentOpenLinkConfirmation(url, target: .inApp) + } + })) + ) + } + + items.append( + .action(ContextMenuActionItem(text: copyText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = cleanUrl + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: isEmail ? presentationData.strings.Conversation_EmailCopied : presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + if canAddToReadingList { + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_AddToReadingList, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + })) + ) + } + + return ContextController.Items(content: .list(items)) } - + self.canReadHistory.set(false) - - let controller = makeContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + + let controller = makeContextController(presentationData: self.presentationData, source: source, items: itemsSignal, recognizer: recognizer, gesture: gesture, disableScreenshots: false) controller.dismissed = { [weak self] in self?.canReadHistory.set(true) } - + self.window?.presentInGlobalOverlay(controller) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 4b66aecd5c..1ee89a7565 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -168,12 +168,8 @@ extension ChatControllerImpl { allReactionsAreAvailable = false } - let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) - if premiumConfiguration.isPremiumDisabled { - allReactionsAreAvailable = false - } - if allReactionsAreAvailable { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) actions.getEmojiContent = { [weak self] animationCache, animationRenderer in guard let self else { preconditionFailure() @@ -188,7 +184,7 @@ extension ChatControllerImpl { hasTrending: false, topReactionItems: reactionItems, areUnicodeEmojiEnabled: false, - areCustomEmojiEnabled: true, + areCustomEmojiEnabled: !premiumConfiguration.isPremiumDisabled, chatPeerId: self.chatLocation.peerId, selectedItems: selectedReactions.files ) 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/ChatControllerOpenPollContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift index 7ce1b07481..ec1ea41250 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollContextMenu.swift @@ -7,6 +7,7 @@ import Display import ContextUI import UndoUI import AccountContext +import ChatMessageBubbleContentNode import ChatMessageItemView import ChatMessageItemCommon import ChatControllerInteraction @@ -42,18 +43,28 @@ extension ChatControllerImpl { if let peerId = pollOption.addedBy { addedByPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) } - + + let messageItemView = params.messageNode as? ChatMessageItemView + let associatedData = (params.messageNode as? ChatMessageBubbleContentNode)?.item?.associatedData ?? messageItemView?.item?.associatedData + let _ = combineLatest( queue: Queue.mainQueue(), - contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: [message], controllerInteraction: self.controllerInteraction, selectAll: false, interfaceInteraction: self.interfaceInteraction, messageNode: params.messageNode as? ChatMessageItemView), + contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: [message], controllerInteraction: self.controllerInteraction, selectAll: false, interfaceInteraction: self.interfaceInteraction, messageNode: messageItemView), addedByPeer ).start(next: { [weak self] actions, addedByPeer in guard let self else { return } - + var items: [ContextMenuItem] = [] - if !poll.isClosed && (selectedOptions.isEmpty || !poll.revotingDisabled) { + + let isRestricted = associatedData?.isPollVotingRestricted( + poll: poll, + accountTestingEnvironment: self.context.account.testingEnvironment, + currentTimestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + ) ?? false + let canVote = poll.pollId.namespace == Namespaces.Media.CloudPoll && !Namespaces.Message.allNonRegular.contains(message.id.namespace) && !isPollEffectivelyClosed(message: EngineMessage(message), poll: poll) && !isRestricted && (selectedOptions.isEmpty || !poll.revotingDisabled) + if canVote { if selectedOptions.contains(pollOption.opaqueIdentifier) { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Poll_RetractOptionVote, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unvote"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in guard let self else { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollOptionMedia.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollOptionMedia.swift index d413ef7a9b..09c907b4b8 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollOptionMedia.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPollOptionMedia.swift @@ -14,6 +14,7 @@ import TelegramStringFormatting import TelegramPresentationData import StickerPeekUI import StickerPackPreviewUI +import PresentationDataUtils extension ChatControllerImpl { func openPollMedia(message: EngineMessage, subject: ChatControllerInteraction.PollMediaSubject) -> Void { @@ -104,6 +105,41 @@ extension ChatControllerImpl { self.presentInGlobalOverlay(peekController) }) + } else if case let .webpage(webpage) = media { + guard let url = webpage.content.url else { + return + } + let _ = self.context.sharedContext.openUserGeneratedUrl( + context: self.context, + peerId: nil, + url: url, + webpage: webpage, + concealed: true, + forceConcealed: true, + skipUrlAuth: true, + skipConcealedAlert: false, + forceDark: false, + present: { [weak self] c in + self?.present(c, in: .window(.root)) + }, + openResolved: { [weak self] result in + guard let self, let navigationController = self.effectiveNavigationController else { + return + } + let context = self.context + context.sharedContext.openResolvedUrl(result, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in + + }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: { _, _, _ in + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }, dismissInput: { + context.sharedContext.mainWindow?.viewController?.view.endEditing(false) + }, contentContext: nil, progress: nil, completion: nil) + }, + progress: nil, + alertDisplayUpdated: nil, + concealedAlertOption: nil + ) } else { let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams( context: self.context, 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/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 3c22fcaffb..1c6b716c02 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -268,7 +268,7 @@ func openWebAppImpl( } else { source = url.isEmpty ? .generic : .simple } - let params = WebAppParameters(source: source, peerId: chatPeer?.id ?? botId, botId: botId, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", appName: "", url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), appSettings: appSettings) + let params = WebAppParameters(source: source, peerId: chatPeer?.id ?? botId, botId: botId, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", appName: "", url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), sameOrigin: result.flags.contains(.sameOrigin), appSettings: appSettings) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: chatPeer?.id ?? botId, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) @@ -317,11 +317,13 @@ func openWebAppImpl( } var presentImpl: ((ViewController, Any?) -> Void)? - let params = WebAppParameters(source: .button, peerId: chatPeer?.id ?? botPeer.id, botId: botPeer.id, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", appName: hasWebApp ? "" : nil, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), appSettings: appSettings) + let params = WebAppParameters(source: .button, peerId: chatPeer?.id ?? botPeer.id, botId: botPeer.id, botName: botName, botVerified: botVerified, botAddress: botPeer.addressName ?? "", appName: hasWebApp ? "" : nil, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), sameOrigin: result.flags.contains(.sameOrigin), appSettings: appSettings) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: chatPeer?.id ?? botPeer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in presentImpl?(c, a) }, commit: commit) + }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in + ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: chatPeer?.id ?? botPeer.id, botAddress: botPeer.addressName ?? "", query: query, chatTypes: chatTypes, completion: completion) }, completion: { [weak parentController] in if let parentController = parentController as? ChatControllerImpl { parentController.chatDisplayNode.historyNode.scrollToEndOfHistory() @@ -398,6 +400,129 @@ func openWebAppImpl( }) } +private final class JoinChatWebViewControllerHolder { + weak var controller: WebAppController? +} + +private func presentJoinChatWebViewDecisionToast(context: AccountContext, parentController: ViewController, presentationData: PresentationData, decision: JoinChatWebViewDecision) { + let present: (EnginePeer?) -> Void = { [weak parentController] peer in + let isBroadcast: Bool + if let peer, case let .channel(channel) = peer, case .broadcast = channel.info { + isBroadcast = true + } else { + isBroadcast = false + } + + let content: UndoOverlayContent + switch decision.result { + case .approved: + if isBroadcast, let peer { + content = .succeed(text: presentationData.strings.Chat_SimilarChannels_JoinedChannel(peer.compactDisplayTitle).string, timeout: nil, customUndoText: nil) + } else { + content = .succeed(text: presentationData.strings.Chat_JoinedGroup_Text, timeout: nil, customUndoText: nil) + } + case .declined: + content = .info(title: nil, text: isBroadcast ? presentationData.strings.Channel_ErrorAccessDenied : presentationData.strings.Group_ErrorAccessDenied, timeout: nil, customUndoText: nil) + case .queued: + content = .inviteRequestSent(title: presentationData.strings.MemberRequests_RequestToJoinSent, text: isBroadcast ? presentationData.strings.MemberRequests_RequestToJoinSentDescriptionChannel : presentationData.strings.MemberRequests_RequestToJoinSentDescriptionGroup) + case .webView: + return + } + + parentController?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + } + + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: decision.peerId)) + |> deliverOnMainQueue).startStandalone(next: { peer in + present(peer) + }) +} + +func openJoinChatWebViewImpl( + context: AccountContext, + parentController: ViewController, + updatedPresentationData: (initial: PresentationData, signal: Signal)?, + webView: JoinChatWebView +) { + if context.isFrozen { + parentController.push(context.sharedContext.makeAccountFreezeInfoScreen(context: context)) + return + } + + let presentationData: PresentationData + if let parentController = parentController as? ChatControllerImpl { + presentationData = parentController.presentationData + } else { + presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + } + + let peerId = webView.peerId ?? webView.botPeer.id + let botName = webView.botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let params = WebAppParameters(source: .button, peerId: peerId, botId: webView.botPeer.id, botName: botName, botVerified: webView.botPeer.isVerified, botAddress: webView.botPeer.addressName ?? "", appName: nil, url: webView.url, queryId: webView.queryId, payload: nil, buttonText: "", keepAliveSignal: nil, forceHasSettings: false, fullSize: false) + + let decisionDisposable = MetaDisposable() + let webAppControllerHolder = JoinChatWebViewControllerHolder() + var pendingUrl: String? + var didHandleTerminalResult = false + var presentImpl: ((ViewController, Any?) -> Void)? + + let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: nil, openUrl: { [weak parentController] url, concealed, forceUpdate, commit in + ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in + presentImpl?(c, a) + }, commit: commit) + }, requestSwitchInline: { [weak parentController] query, chatTypes, completion in + ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peerId, botAddress: webView.botPeer.addressName ?? "", query: query, chatTypes: chatTypes, completion: completion) + }, didDismiss: { + decisionDisposable.dispose() + }, getNavigationController: { [weak parentController] in + var navigationController: NavigationController? + if let parentController = parentController as? ChatControllerImpl { + navigationController = parentController.effectiveNavigationController + } + return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController) + }, onControllerCreated: { controller in + webAppControllerHolder.controller = controller + if let pendingUrl { + controller.loadExternal(url: pendingUrl) + } + pendingUrl = nil + }) + controller.navigationPresentation = .flatModal + + presentImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + + decisionDisposable.set((context.account.stateManager.joinChatWebViewDecisions + |> deliverOnMainQueue).start(next: { [weak controller, weak parentController] decisions in + for decision in decisions { + guard decision.queryId == webView.queryId else { + continue + } + switch decision.result { + case let .webView(url): + if let webAppController = webAppControllerHolder.controller { + webAppController.loadExternal(url: url) + } else { + pendingUrl = url + } + case .approved, .declined, .queued: + if didHandleTerminalResult { + return + } + didHandleTerminalResult = true + decisionDisposable.set(nil) + controller?.dismiss(animated: true) + if let parentController { + presentJoinChatWebViewDecisionToast(context: context, parentController: parentController, presentationData: presentationData, decision: decision) + } + } + } + })) + + parentController.push(controller) +} + public extension ChatControllerImpl { func openWebApp(buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { @@ -500,7 +625,7 @@ public extension ChatControllerImpl { if let controller { controller.openUrl(url, concealed: concealed, forceExternal: true, commit: commit) } else { - let _ = openUserGeneratedUrl(context: context, peerId: peerId, url: url, concealed: concealed, present: { c in + let _ = context.sharedContext.openUserGeneratedUrl(context: context, peerId: peerId, url: url, webpage: nil, concealed: concealed, forceConcealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: false, present: { c in present(c, nil) }, openResolved: { result in var navigationController: NavigationController? @@ -522,7 +647,7 @@ public extension ChatControllerImpl { context.sharedContext.mainWindow?.viewController?.view.endEditing(false) }, contentContext: nil, progress: nil, completion: nil) } - }) + }, progress: nil, alertDisplayUpdated: nil, concealedAlertOption: nil) } } @@ -607,7 +732,7 @@ public extension ChatControllerImpl { updateProgress() }) |> deliverOnMainQueue).startStandalone(next: { [weak parentController, weak chatController] result in - let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, botAddress: botPeer.addressName ?? "", appName: botApp.shortName, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), appSettings: appSettings) + let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, botAddress: botPeer.addressName ?? "", appName: botApp.shortName, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize), isFullscreen: result.flags.contains(.fullScreen), sameOrigin: result.flags.contains(.sameOrigin), appSettings: appSettings) var presentImpl: ((ViewController, Any?) -> Void)? let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { url, concealed, forceUpdate, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: chatController, url: url, concealed: concealed, forceUpdate: forceUpdate, present: { c, a in diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index c55fc79bda..ff9467e7ef 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -204,6 +204,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/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index d1ee4cda89..9ccb490700 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -572,7 +572,7 @@ func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubj var suggestedPeers: [EnginePeer] = [] if let message = await selfController.context.engine.data.get( TelegramEngine.EngineData.Item.Messages.Message(id: replySubject.messageId) - ).get(), case let .user(author) = message.author { + ).get(), case let .user(author) = message.author, author.id != selfController.context.account.peerId { suggestedPeers.append(.user(author)) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b2fe984711..c5018fafd8 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -484,6 +484,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G weak var emojiPackTooltipController: TooltipScreen? weak var birthdayTooltipController: TooltipScreen? weak var scheduledVideoProcessingTooltipController: TooltipScreen? + weak var guestChatMessageTooltipController: TooltipScreen? weak var slowmodeTooltipController: ChatSlowmodeHintController? @@ -1588,17 +1589,34 @@ 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 + }, photoToolbarView: { [context = self.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) + }, 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) @@ -2219,7 +2237,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } } - }, sendMessage: { [weak self] text in + }, sendMessage: { [weak self] text, sourceMessageId in guard let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) else { return } @@ -2247,12 +2265,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } + let replyMessageSubject = sourceMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) + } ?? strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel let peerId = strongSelf.chatLocation.peerId if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = strongSelf.chatDisplayNode.interactiveEmojis, interactiveEmojis.emojis.contains(text) { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: text)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: text)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } else { - strongSelf.sendMessages([.message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: text, attributes: attributes, inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets in guard let strongSelf = self else { @@ -3281,7 +3302,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } }) - }, shareCurrentLocation: { [weak self] in + }, shareCurrentLocation: { [weak self] sourceMessageId in if let strongSelf = self { if case .pinnedMessages = strongSelf.presentationInterfaceState.subject { return @@ -3296,7 +3317,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> deliverOnMainQueue).startStandalone(next: { coordinate in if let strongSelf = self { if let coordinate = coordinate { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + let replyMessageSubject = sourceMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) + } + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: replyMessageSubject, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})]), in: .window(.root)) } @@ -3305,7 +3329,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })]), in: .window(.root)) } - }, shareAccountContact: { [weak self] in + }, shareAccountContact: { [weak self] sourceMessageId in if let strongSelf = self { if case .pinnedMessages = strongSelf.presentationInterfaceState.subject { return @@ -3327,7 +3351,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } |> deliverOnMainQueue).startStandalone(next: { peer in if case let .user(user) = peer, let phone = user.phone, !phone.isEmpty { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + let replyMessageSubject = sourceMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) + } + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }) } @@ -3656,6 +3683,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .none } } + if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject, case .quickReplyMessageInput = customChatContents.kind { + return .none + } if canReplyInChat(strongSelf.presentationInterfaceState, accountPeerId: strongSelf.context.account.peerId) { return .reply @@ -4422,12 +4452,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) }) - }, openPollCreation: { [weak self] isQuiz in + }, openPollCreation: { [weak self] sourceMessageId, isQuiz in guard let self else { return } let _ = self.presentVoiceMessageDiscardAlert(action: { - if let controller = self.configurePollCreation(isQuiz: isQuiz) { + if let controller = self.configurePollCreation(sourceMessageId: sourceMessageId, isQuiz: isQuiz) { controller.navigationPresentation = .modal self.effectiveNavigationController?.pushViewController(controller) } @@ -4649,6 +4679,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText legacyMediaEditor(context: strongSelf.context, peer: EnginePeer(peer), threadTitle: strongSelf.contentData?.state.threadInfo?.title, media: mediaReference, mode: .draw, initialCaption: inputText, snapshots: [], transitionCompletion: nil, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: true) + }, photoToolbarView: { [context = strongSelf.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }, sendMessagesWithSignals: { [weak self] signals, _, _, _ in if let strongSelf = self { strongSelf.interfaceInteraction?.setupEditMessage(messageId, { _ in }) @@ -5214,14 +5246,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.joinChannelDisposable.set(( self.context.peerChannelMemberCategoriesContextsManager.join(engine: self.context.engine, peerId: peer.id, hash: nil) |> deliverOnMainQueue - |> afterCompleted { [weak self] in - Queue.mainQueue().async { - if let self { - self.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Chat_SimilarChannels_JoinedChannel(peer.compactDisplayTitle).string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - } + ).startStrict(next: { [weak self] result in + guard let self else { + return } - ).startStrict(error: { [weak self] error in + switch result { + case .joined: + self.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Chat_SimilarChannels_JoinedChannel(peer.compactDisplayTitle).string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { _ in return false }), in: .current) + case let .webView(webView): + self.context.sharedContext.openJoinChatWebView(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, webView: webView) + } + }, error: { [weak self] error in guard let self else { return } @@ -7743,13 +7778,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.peekTimerDisposable.set( (strongSelf.context.engine.peers.joinChatInteractively(with: peekData.linkData) - |> deliverOnMainQueue).startStrict(next: { result in + |> deliverOnMainQueue).startStrict(next: { [weak self] result in guard let strongSelf = self else { return } - if case let .joined(peer) = result, peer != nil { - } else { - strongSelf.dismiss() + switch result { + case let .joined(peer): + if peer == nil { + strongSelf.dismiss() + } + case let .webView(webView): + strongSelf.context.sharedContext.openJoinChatWebView(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, webView: webView) } }, error: { _ in guard let strongSelf = self else { @@ -9414,36 +9453,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser else { return } - - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumberConfirmation(formatPhoneNumber(context: strongSelf.context, number: phoneNumber), EnginePeer(peer).compactDisplayTitle).string)) - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - guard let strongSelf = self else { - return - } - let _ = (strongSelf.context.engine.contacts.acceptAndShareContact(peerId: peer.id) - |> deliverOnMainQueue).startStandalone(error: { _ in - guard let strongSelf = self else { - return - } - strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, completed: { - guard let strongSelf = self else { - return - } - strongSelf.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .genericSuccess(strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber_StatusSuccess(EnginePeer(peer).compactDisplayTitle).string, true)), in: .window(.root)) - }) - })) - - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) + let alertController = textAlertController( + context: strongSelf.context, + title: nil, + text: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumberConfirmation(formatPhoneNumber(context: strongSelf.context, number: phoneNumber), EnginePeer(peer).compactDisplayTitle).string, + actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber, action: { + guard let strongSelf = self else { + return + } + let _ = (strongSelf.context.engine.contacts.acceptAndShareContact(peerId: peer.id) + |> deliverOnMainQueue).startStandalone(error: { _ in + guard let strongSelf = self else { + return + } + strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { + guard let strongSelf = self else { + return + } + strongSelf.present(OverlayStatusController(theme: strongSelf.presentationData.theme, type: .genericSuccess(strongSelf.presentationData.strings.Conversation_ShareMyPhoneNumber_StatusSuccess(EnginePeer(peer).compactDisplayTitle).string, true)), in: .window(.root)) + }) + }) + ] + ) + strongSelf.present(alertController, in: .window(.root)) }) } @@ -9690,17 +9725,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let self else { return } - let disposable = openUserGeneratedUrl(context: self.context, peerId: self.contentData?.state.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in + let disposable = self.context.sharedContext.openUserGeneratedUrl(context: self.context, peerId: self.contentData?.state.peerView?.peerId, url: url, webpage: nil, concealed: concealed, forceConcealed: false, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, forceDark: false, present: { [weak self] c in self?.present(c, in: .window(.root)) }, openResolved: { [weak self] resolved in self?.openResolved(result: resolved, sourceMessageId: message?.id, progress: progress, forceExternal: forceExternal, concealed: concealed, commit: commit) - }, progress: progress) + }, progress: progress, alertDisplayUpdated: nil, concealedAlertOption: nil) self.navigationActionDisposable.set(disposable) }, performAction: true) } func openUrlIn(_ url: String) { - let actionSheet = OpenInActionSheetController(context: self.context, updatedPresentationData: self.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in + let actionSheet = OpenInOptionsScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, item: .url(url: url), openUrl: { [weak self] url in if let strongSelf = self, let navigationController = strongSelf.effectiveNavigationController { strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { self?.chatDisplayNode.dismissInput() @@ -9708,7 +9743,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }) self.chatDisplayNode.dismissInput() - self.present(actionSheet, in: .window(.root)) + self.push(actionSheet) } @available(iOSApplicationExtension 11.0, iOS 11.0, *) @@ -10150,6 +10185,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.mediaRestrictedTooltipController?.dismiss() self.checksTooltipController?.dismiss() self.copyProtectionTooltipController?.dismiss() + self.guestChatMessageTooltipController?.dismiss() self.window?.forEachController({ controller in if let controller = controller as? UndoOverlayController { @@ -10429,13 +10465,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 } @@ -10473,7 +10509,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/ChatControllerDisplayGuestChatMessageTooltip.swift b/submodules/TelegramUI/Sources/ChatControllerDisplayGuestChatMessageTooltip.swift new file mode 100644 index 0000000000..adcaf3a277 --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatControllerDisplayGuestChatMessageTooltip.swift @@ -0,0 +1,58 @@ +import Foundation +import TelegramPresentationData +import AccountContext +import TelegramCore +import SwiftSignalKit +import Display +import PresentationDataUtils +import ChatMessageItemView +import TelegramNotices +import TooltipUI + +extension ChatControllerImpl { + func displayGuestChatMessageTooltip(itemNode: ChatMessageItemView) { + let _ = (ApplicationSpecificNotice.getGuestChatMessageTooltip(accountManager: self.context.sharedContext.accountManager) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak itemNode] value in + guard let self, let itemNode else { + return + } + + if value >= 2 { + return + } + + guard let sourceNode = itemNode.getAuthorNameNode() else { + return + } + + Queue.mainQueue().after(0.5) { + let sourceRect = sourceNode.view.convert(sourceNode.view.bounds, to: nil) + + self.messageTooltipController?.dismiss() + self.guestChatMessageTooltipController?.dismiss() + + let tooltipScreen = TooltipScreen( + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .plain(text: self.presentationData.strings.Chat_GuestChatMessageTooltip), + balancedTextLayout: true, + location: .point(sourceRect, .bottom), + displayDuration: .custom(3.5), + shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + } + ) + self.guestChatMessageTooltipController = tooltipScreen + tooltipScreen.becameDismissed = { [weak self, weak tooltipScreen] _ in + if let strongSelf = self, let tooltipScreen, strongSelf.guestChatMessageTooltipController === tooltipScreen { + strongSelf.guestChatMessageTooltipController = nil + } + } + + self.present(tooltipScreen, in: .current) + + let _ = ApplicationSpecificNotice.incrementGuestChatMessageTooltip(accountManager: self.context.sharedContext.accountManager).startStandalone() + } + }) + } +} diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index d8c4dea9aa..2588a90e7a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -20,7 +20,6 @@ import MediaPickerUI import LegacyCamera import LegacyMediaPickerUI import LocationUI -import WebSearchUI import WebUI import UndoUI import ICloudResources @@ -1076,14 +1075,13 @@ extension ChatControllerImpl { } }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: false) + }, photoToolbarView: { [context = strongSelf.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }) } }, openFileGallery: { self?.presentFileMediaPickerOptions(editingMessage: true) - }, openWebSearch: { [weak self] in - self?.presentWebSearch(editingMessage: editMediaOptions != nil, attachment: false, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) + }, openWebSearch: { }, openMap: { self?.presentLocationPicker() }, openContacts: { @@ -1495,32 +1493,7 @@ extension ChatControllerImpl { legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] - configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in - if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting, scheduleTime in - if let legacyController = legacyController { - legacyController.dismiss() - } - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in - if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }, enqueueMediaMessages: { signals in - if let strongSelf = self { - if editingMedia { - strongSelf.editMessageMediaWithLegacySignals(signals) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - } - }) - })) - controller.getCaptionPanelView = { [weak self] in - return self?.getCaptionPanelView(isFile: fileMode) - } - strongSelf.effectiveNavigationController?.pushViewController(controller) - } - }, presentSelectionLimitExceeded: { + configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: nil, presentSelectionLimitExceeded: { guard let strongSelf = self else { return } @@ -1572,120 +1545,6 @@ extension ChatControllerImpl { }) } - func presentWebSearch(editingMessage: Bool, attachment: Bool, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { - guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { - return - } - - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) - |> deliverOnMainQueue).startStandalone(next: { [weak self] configuration in - if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in - self?.attachmentController?.dismiss(animated: true, completion: nil) - legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in - if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - }, enqueueMediaMessages: { [weak self] signals in - if let strongSelf = self, !signals.isEmpty { - if editingMessage { - strongSelf.editMessageMediaWithLegacySignals(signals) - } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) - } - } - }) - }), activateOnDisplay: activateOnDisplay) - controller.attemptItemSelection = { [weak strongSelf] item in - guard let strongSelf, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { - return false - } - - enum ItemType { - case gif - case image - case video - } - - var itemType: ItemType? - switch item { - case let .internalReference(reference): - if reference.type == "gif" { - itemType = .gif - } else if reference.type == "photo" { - itemType = .image - } else if reference.type == "video" { - itemType = .video - } - case let .externalReference(reference): - if reference.type == "gif" { - itemType = .gif - } else if reference.type == "photo" { - itemType = .image - } else if reference.type == "video" { - itemType = .video - } - } - - var bannedSendPhotos: (Int32, Bool)? - var bannedSendVideos: (Int32, Bool)? - var bannedSendGifs: (Int32, Bool)? - - if let channel = peer as? TelegramChannel { - if let value = channel.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = value - } - if let value = channel.hasBannedPermission(.banSendVideos) { - bannedSendVideos = value - } - if let value = channel.hasBannedPermission(.banSendGifs) { - bannedSendGifs = value - } - } else if let group = peer as? TelegramGroup { - if group.hasBannedPermission(.banSendPhotos) { - bannedSendPhotos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendVideos) { - bannedSendVideos = (Int32.max, false) - } - if group.hasBannedPermission(.banSendGifs) { - bannedSendGifs = (Int32.max, false) - } - } - - if let itemType { - switch itemType { - case .image: - if bannedSendPhotos != nil { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - - return false - } - case .video: - if bannedSendVideos != nil { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - - return false - } - case .gif: - if bannedSendGifs != nil { - strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - - return false - } - } - } - - return true - } - controller.getCaptionPanelView = { [weak strongSelf] in - return strongSelf?.getCaptionPanelView(isFile: false) - } - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - }) - } - func presentLocationPicker() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return @@ -1704,7 +1563,7 @@ extension ChatControllerImpl { return } let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.subject != .scheduledMessages - let controller = LocationPickerController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in + let controller = LocationPickerController(context: strongSelf.context, style: .glass, updatedPresentationData: strongSelf.updatedPresentationData, mode: .share(peer: EnginePeer(peer), selfPeer: selfPeer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _, _, _, _ in guard let strongSelf = self else { return } @@ -1970,7 +1829,7 @@ extension ChatControllerImpl { } }, presentSchedulePicker: { [weak self] _, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in + strongSelf.presentScheduleTimePicker(style: .media, presentInOverlay: true, completion: { [weak self] result in if let strongSelf = self { done(result.time, result.silentPosting) if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { @@ -1987,6 +1846,8 @@ extension ChatControllerImpl { } }, getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: false) + }, photoToolbarView: { [context = strongSelf.context] backButton, doneButton, solidBackground, hasSendStarsButton in + return makeMediaPickerPhotoToolbarView(context: context, backButton: backButton, doneButton: doneButton, solidBackground: solidBackground, hasSendStarsButton: hasSendStarsButton) }, dismissedWithResult: { [weak self] in self?.attachmentController?.dismiss(animated: false, completion: nil) }, finishedTransitionIn: { [weak self] in @@ -2083,7 +1944,7 @@ extension ChatControllerImpl { self.push(mainController) } - func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? { + func configurePollCreation(sourceMessageId: EngineMessage.Id? = nil, isQuiz: Bool? = nil) -> ViewController? { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return nil } @@ -2100,7 +1961,9 @@ extension ChatControllerImpl { guard let self else { return } - let replyMessageSubject = self.presentationInterfaceState.interfaceState.replyMessageSubject + let replyMessageSubject = sourceMessageId.flatMap { + EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) + } ?? self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let self { self.chatDisplayNode.collapseInput() @@ -2149,7 +2012,7 @@ extension ChatControllerImpl { correlationId: nil, bubbleUpEmojiOrStickersets: [] ) - self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel)]) + self.sendMessages([message.withUpdatedReplyToMessageId(replyMessageSubject)]) }) } ) diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 9673ce7981..7cdc841d5e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -761,6 +761,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH var frozenMessageForScrollingReset: EngineMessage.Id? private var hasDisplayedBusinessBotMessageTooltip: Bool = false + private var hasDisplayedGuestChatMessageTooltip: Bool = false private let _isReady = ValuePromise(false, ignoreRepeated: true) public var isReady: Signal { @@ -2909,6 +2910,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH var visibleAdOpaqueIds: [Data] = [] var peerIdsWithRefreshStories: [PeerId] = [] var visibleBusinessBotMessageId: EngineMessage.Id? + var visibleGuestChatMessageId: EngineMessage.Id? if indexRange.0 <= indexRange.1 { for i in (indexRange.0 ... indexRange.1) { @@ -3127,6 +3129,15 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH visibleBusinessBotMessageId = message.id } } + if message.id.namespace == Namespaces.Message.Cloud, message.flags.contains(.Incoming), message.guestChatAttribute != nil { + if let visibleGuestChatMessageIdValue = visibleGuestChatMessageId { + if visibleGuestChatMessageIdValue < message.id { + visibleGuestChatMessageId = message.id + } + } else { + visibleGuestChatMessageId = message.id + } + } switch message.id.peerId.namespace { case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: messageIdsWithPossibleReactions.append(message.id) @@ -3144,6 +3155,15 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH visibleBusinessBotMessageId = message.id } } + if message.id.namespace == Namespaces.Message.Cloud, message.flags.contains(.Incoming), message.guestChatAttribute != nil { + if let visibleGuestChatMessageIdValue = visibleGuestChatMessageId { + if visibleGuestChatMessageIdValue < message.id { + visibleGuestChatMessageId = message.id + } + } else { + visibleGuestChatMessageId = message.id + } + } switch message.id.peerId.namespace { case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel: messageIdsWithPossibleReactions.append(message.id) @@ -3340,6 +3360,23 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH } } } + + if let visibleGuestChatMessageId, !self.hasDisplayedGuestChatMessageTooltip { + var foundItemNode: ChatMessageItemView? + self.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == visibleGuestChatMessageId, itemNode.getAuthorNameNode() != nil { + foundItemNode = itemNode + } + } + + if let foundItemNode { + self.hasDisplayedGuestChatMessageTooltip = true + + if let controllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, let chatController = controllerNode.interfaceInteraction?.chatController() as? ChatControllerImpl { + chatController.displayGuestChatMessageTooltip(itemNode: foundItemNode) + } + } + } } if !self.isSettingTopReplyThreadMessageShown { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 4d6adbe777..873d196f6f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -497,11 +497,18 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if case .standard(.embedded) = chatPresentationInterfaceState.mode { isEmbeddedMode = true } - + if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject, case .hashTagSearch = customChatContents.kind { isEmbeddedMode = true } - + + let isSharedMediaPolls: Bool + if isEmbeddedMode, case let .tag(tag)? = chatPresentationInterfaceState.subject, tag == .polls { + isSharedMediaPolls = true + } else { + isSharedMediaPolls = false + } + var hasExpandedAudioTranscription = false if let messageNode = messageNode as? ChatMessageBubbleItemNode { hasExpandedAudioTranscription = messageNode.hasExpandedAudioTranscription() @@ -942,9 +949,36 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState |> deliverOnMainQueue |> map { data, updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList, accountPeer -> ContextController.Items in let isPremium = accountPeer?.isPremium ?? false - + var actions: [ContextMenuItem] = [] - + + if isSharedMediaPolls && messages.count == 1 { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.SharedMedia_ViewInChat, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.actionSheet.primaryTextColor) + }, action: { c, _ in + c?.dismiss(completion: { + guard let navigationController = controllerInteraction.navigationController() else { + return + } + guard let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init) else { + return + } + if case let .channel(channel) = peer, channel.isForumOrMonoForum, let threadId = message.threadId { + let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .default, animated: true).startStandalone() + } else { + let targetLocation: NavigateToChatControllerParams.Location + if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { + targetLocation = .replyThread(replyThreadMessage) + } else { + targetLocation = .peer(peer) + } + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: targetLocation, subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: true)) + } + }) + }))) + } + var isPinnedMessages = false if case .pinnedMessages = chatPresentationInterfaceState.subject { isPinnedMessages = true diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index c74d28f32b..c5e4ae8176 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -66,7 +66,7 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha if let currentButton = currentButton, currentButton.action == .dismiss { return currentButton } else { - let buttonItem = UIBarButtonItem(title: strings.Common_Close, style: .plain, target: target, action: selector) + let buttonItem = UIBarButtonItem(title: "___close", style: .plain, target: target, action: selector) buttonItem.accessibilityLabel = strings.Common_Close return ChatNavigationButton(action: .dismiss, buttonItem: buttonItem) } diff --git a/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatInviteRequestsTitlePanelNode.swift index bd86622a5a..47ac7716c8 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 @@ -201,7 +106,18 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { if interfaceState.theme !== self.theme { self.theme = interfaceState.theme - self.closeButton.setImage(PresentationResourcesChat.chatInputPanelEncircledCloseIconImage(interfaceState.theme), for: []) + self.closeButton.setImage(generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(interfaceState.theme.chat.inputPanel.panelControlColor.cgColor) + context.setLineWidth(1.33) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() + }), for: []) self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } @@ -210,14 +126,14 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { let contentRightInset: CGFloat = 14.0 + rightInset let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width - 3.0, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)) - self.buttonTitle.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RequestsToJoin(self.count), font: Font.regular(16.0), textColor: interfaceState.theme.rootController.navigationBar.accentTextColor) + self.buttonTitle.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_RequestsToJoin(self.count), font: Font.medium(15.0), textColor: interfaceState.theme.chat.inputPanel.panelControlColor) transition.updateFrame(node: self.button, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: panelHeight))) let titleSize = self.buttonTitle.updateLayout(CGSize(width: width - leftInset - 90.0 - contentRightInset, height: 100.0)) - var buttonTitleFrame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - titleSize.width) * 0.5), y: floor((panelHeight - titleSize.height) * 0.5)), size: titleSize) + var buttonTitleFrame = CGRect(origin: CGPoint(x: leftInset + floor((width - leftInset - titleSize.width) * 0.5), y: floor((panelHeight - titleSize.height) * 0.5) + 1.0), size: titleSize) buttonTitleFrame.origin.x = max(buttonTitleFrame.minX, leftInset + 90.0) transition.updatePosition(node: self.buttonTitle, position: buttonTitleFrame.origin) self.buttonTitle.bounds = CGRect(origin: CGPoint(), size: buttonTitleFrame.size) @@ -227,7 +143,7 @@ final class ChatInviteRequestsTitlePanelNode: ChatTitleAccessoryPanelNode { if let avatarsContent = self.avatarsContent { let avatarsSize = self.avatarsNode.update(context: self.context, content: avatarsContent, itemSize: CGSize(width: 32.0, height: 32.0), animated: true, synchronousLoad: true) - transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: leftInset + 8.0, y: floor((panelHeight - avatarsSize.height) / 2.0)), size: avatarsSize)) + transition.updateFrame(node: self.avatarsNode, frame: CGRect(origin: CGPoint(x: leftInset + 4.0, y: floor((panelHeight - avatarsSize.height) / 2.0)), size: avatarsSize)) } self.activateAreaNode.frame = CGRect(origin: .zero, size: CGSize(width: width, height: panelHeight)) diff --git a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift index ee3f739586..1467fa6d33 100644 --- a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift @@ -97,9 +97,9 @@ private final class ChatManagingBotTitlePanelComponent: Component { let topInset: CGFloat = 6.0 let bottomInset: CGFloat = 6.0 let avatarDiameter: CGFloat = 36.0 - let avatarTextSpacing: CGFloat = 10.0 + let avatarTextSpacing: CGFloat = 8.0 let titleTextSpacing: CGFloat = 1.0 - let leftInset: CGFloat = component.insets.left + 12.0 + let leftInset: CGFloat = component.insets.left + 6.0 let rightInset: CGFloat = component.insets.right + 10.0 let actionAndSettingsButtonsSpacing: CGFloat = 8.0 @@ -134,7 +134,7 @@ private final class ChatManagingBotTitlePanelComponent: Component { component: AnyComponent(PlainButtonComponent( content: AnyComponent(BundleIconComponent( name: "Chat/Context Menu/Customize", - tintColor: component.theme.rootController.navigationBar.controlColor + tintColor: component.theme.chat.inputPanel.panelControlColor )), effectAlignment: .center, minSize: CGSize(width: 1.0, height: 40.0), @@ -164,7 +164,7 @@ private final class ChatManagingBotTitlePanelComponent: Component { let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.semibold(16.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)) + text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.semibold(16.0), textColor: component.theme.chat.inputPanel.panelControlColor)) )), environment: {}, containerSize: CGSize(width: maxTextWidth, height: 100.0) @@ -178,7 +178,7 @@ private final class ChatManagingBotTitlePanelComponent: Component { let textSize = self.text.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: textValue, font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor)) + text: .plain(NSAttributedString(string: textValue, font: Font.regular(14.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor)) )), environment: {}, containerSize: CGSize(width: maxTextWidth, height: 100.0) diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 61744d861e..b4bc3caa51 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -934,7 +934,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let button = attribute.rows[0].buttons[0] switch button.action { case .text: - controllerInteraction.sendMessage(button.title) + controllerInteraction.sendMessage(button.title, message.id) return case let .url(url): var isConcealed = true @@ -944,10 +944,10 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: isConcealed, progress: Promise())) return case .requestMap: - controllerInteraction.shareCurrentLocation() + controllerInteraction.shareCurrentLocation(message.id) return case .requestPhone: - controllerInteraction.shareAccountContact() + controllerInteraction.shareAccountContact(message.id) return case .openWebApp: let progressPromise = Promise() 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/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index be9444e201..b29ac66d01 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -288,6 +288,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe }, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in + }, performBotConnectionReviewAction: { _, _ in }, openChatFolderUpdates: { }, hideChatFolderUpdates: { }, openStories: { _, _ in 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/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 0a652d4f18..2accdfc015 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -166,6 +166,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { }, performActiveSessionAction: { _, _ in }, + performBotConnectionReviewAction: { _, _ in + }, openChatFolderUpdates: { }, hideChatFolderUpdates: { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index b0d6291295..908f39f5e6 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -298,17 +298,17 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection case .peerSelection: self.titleView.title = CounterControllerTitle(title: self.params.title ?? self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "") if self.rightNavigationButton == nil { - let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) + let rightNavigationButton = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) self.rightNavigationButton = rightNavigationButton - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(cancelPressed)) self.navigationItem.rightBarButtonItem = self.rightNavigationButton } case let .chatSelection(chatSelection): self.titleView.title = CounterControllerTitle(title: self.params.title ?? chatSelection.title, counter: "") if self.rightNavigationButton == nil { - let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) + let rightNavigationButton = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) self.rightNavigationButton = rightNavigationButton - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(cancelPressed)) self.navigationItem.rightBarButtonItem = self.rightNavigationButton } } diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index d272c478ae..6c958947a2 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -280,7 +280,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { } let inset: CGFloat - if layout.size.width >= 375.0 { + if layout.size.width >= 320.0 { inset = max(16.0, floor((layout.size.width - 674.0) / 2.0)) } else { inset = 0.0 diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index 3345411ed1..a8deff185a 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -13,7 +13,6 @@ import AlertUI import PresentationDataUtils import LegacyUI import ItemListAvatarAndNameInfoItem -import WebSearchUI import PeerInfoUI import MapResourceToAvatarSizes import LegacyMediaPickerUI @@ -493,7 +492,7 @@ public func createChannelController(context: AccountContext, mode: CreateChannel keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) var dismissPickerImpl: (() -> Void)? - let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, peerType: .channel, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { clearPendingAvatar() }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in avatarPickerHolder = nil diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index 14ecb0c14b..5acbb90d51 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -20,7 +20,6 @@ import LegacyUI import LocationUI import ItemListPeerItem import ItemListAvatarAndNameInfoItem -import WebSearchUI import Geocoding import PeerInfoUI import MapResourceToAvatarSizes @@ -923,7 +922,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) var dismissPickerImpl: (() -> Void)? - let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, peerType: .group, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { clearPendingAvatar() }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in avatarPickerHolder = nil @@ -1043,32 +1042,32 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] }, changeLocation: { endEditingImpl?() - let controller = LocationPickerController(context: context, mode: .pick, completion: { location, _, _, address, _ in - 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 _ = (addressSignal - |> deliverOnMainQueue).start(next: { address in - addressPromise.set(.single(address)) - updateState { current in - var current = current - current.location = PeerGeoLocation(latitude: location.latitude, longitude: location.longitude, address: address) - return current - } - }) - }) - pushImpl?(controller) + let controller = LocationPickerController(context: context, style: .glass, mode: .pick, completion: { location, _, _, address, _ in + 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 _ = (addressSignal + |> deliverOnMainQueue).start(next: { address in + addressPromise.set(.single(address)) + updateState { current in + var current = current + current.location = PeerGeoLocation(latitude: location.latitude, longitude: location.longitude, address: address) + return current + } + }) + }) + pushImpl?(controller) }, updateWithVenue: { venue in guard let venueData = venue.venue else { return diff --git a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift index 579bf2383d..84715bc9dc 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift @@ -271,7 +271,7 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { strongSelf.activateAreaNode.accessibilityLabel = item.title strongSelf.activateAreaNode.frame = CGRect(origin: .zero, size: nodeLayout.size) - strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)]) strongSelf.setRevealOptionsOpened(item.revealed, animated: animation.isAnimated) } }) diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index e02fd3fa31..16bec6dad9 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -82,6 +82,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, ASScrollVie self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.scrollNode.view.scrollsToTop = false self.addSubnode(self.scrollNode) } diff --git a/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift b/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift index 168e00da12..b30613f214 100644 --- a/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift @@ -221,7 +221,7 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { strongSelf.activateAreaNode.frame = CGRect(origin: .zero, size: nodeLayout.size) if case let .user(peer) = item.peer, let _ = peer.botInfo { - strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]) + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)]) strongSelf.setRevealOptionsOpened(item.revealed, animated: animation.isAnimated) } else { strongSelf.setRevealOptions([]) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index d448a26b79..7ed2e227bf 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1944,6 +1944,8 @@ func openResolvedUrlImpl( browserIdentifier = "duckDuckGo" } else if browser.hasPrefix("Alook") { browserIdentifier = "alook" + } else if browser.hasPrefix("Vivaldi") { + browserIdentifier = "vivaldi" } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 08824b024f..685b93753a 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -1,6 +1,5 @@ import Foundation import Display -import SafariServices import TelegramCore import SwiftSignalKit import MtProtoKit @@ -254,27 +253,22 @@ private func handleInternetUrl( if let host = parsedUrl.host, telegramMeHosts.contains(host) { handleInternalUrl(parsedUrl.absoluteString) } else { - let settings = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings, ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), context.sharedContext.accountManager.accessChallengeData()) + let settings = combineLatest( + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.webBrowserSettings)) + ) |> take(1) - |> map { sharedData, accessChallengeData -> WebBrowserSettings in - let passcodeSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]?.get(PresentationPasscodeSettings.self) ?? PresentationPasscodeSettings.defaultSettings - - var settings: WebBrowserSettings - if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) { - settings = current - } else { - settings = .defaultSettings - } - if accessChallengeData.data.isLockable { - if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == "inApp" { - settings = WebBrowserSettings(defaultWebBrowser: "safari", exceptions: []) - } - } - return settings + |> map { sharedData, accountSettingsEntry -> (WebBrowserSettings, AccountWebBrowserSettings) in + let localSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings + let accountSettings = accountSettingsEntry?.get(AccountWebBrowserSettings.self) ?? AccountWebBrowserSettings.defaultSettings + return (localSettings, accountSettings) } let _ = (settings |> deliverOnMainQueue).startStandalone(next: { settings in + let localSettings = settings.0 + let accountSettings = settings.1 + var isTonSite = false if let host = parsedUrl.host, host.lowercased().hasSuffix(".ton") { isTonSite = true @@ -282,9 +276,35 @@ private func handleInternetUrl( isTonSite = true } - if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" && !isTonSite { + var isExceptedDomain = false + let host = ".\((parsedUrl.host ?? "").lowercased())" + let exceptions = accountSettings.openExternalBrowser ? accountSettings.inAppExceptions : accountSettings.externalExceptions + for exception in exceptions { + if host.hasSuffix(".\(exception.domain.lowercased())") { + isExceptedDomain = true + break + } + } + + let shouldOpenInApp: Bool + if isTonSite { + shouldOpenInApp = true + } else if accountSettings.openExternalBrowser { + shouldOpenInApp = isExceptedDomain + } else { + shouldOpenInApp = !isExceptedDomain + } + + if shouldOpenInApp { + let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) + navigationController?.pushViewController(controller) + } else { let openInOptions = availableOpenInOptions(context: context, item: .url(url: originalUrl)) - if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) { + var defaultWebBrowser = localSettings.defaultWebBrowser + if defaultWebBrowser == nil || defaultWebBrowser == "inApp" || defaultWebBrowser == "inAppSafari" { + defaultWebBrowser = "safari" + } + if let option = openInOptions.first(where: { $0.identifier == defaultWebBrowser }) { if case let .openUrl(openInUrl) = option.action() { context.sharedContext.applicationBindings.openUrl(openInUrl) } else { @@ -293,29 +313,6 @@ private func handleInternetUrl( } else { context.sharedContext.applicationBindings.openUrl(originalUrl) } - } else { - var isExceptedDomain = false - let host = ".\((parsedUrl.host ?? "").lowercased())" - for exception in settings.exceptions { - if host.hasSuffix(".\(exception.domain)") { - isExceptedDomain = true - break - } - } - - if (settings.defaultWebBrowser == nil && !isExceptedDomain) || isTonSite { - let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) - navigationController?.pushViewController(controller) - } else { - if let window = navigationController?.view.window, !isExceptedDomain { - let controller = SFSafariViewController(url: parsedUrl) - controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor - controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor - window.rootViewController?.present(controller, animated: true) - } else { - context.sharedContext.applicationBindings.openUrl(parsedUrl.absoluteString) - } - } } }) } @@ -1017,7 +1014,16 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let host = parsedUrl.host, telegramMeHosts.contains(host) { continueHandling() } else { - if isTelegraPhLink(parsedUrl.absoluteString) { + if isTelegramWebShortLink(parsedUrl.absoluteString) { + handleInternetUrl( + parsedUrl: parsedUrl, + originalUrl: url, + context: context, + presentationData: presentationData, + navigationController: navigationController, + handleInternalUrl: handleInternalUrl + ) + } else if isTelegraPhLink(parsedUrl.absoluteString) { continueHandling() } else { context.sharedContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 7dcccfc6ba..e23bad464c 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -141,7 +141,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, tapMessage: nil, clickThroughMessage: { _, _ in }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in - }, sendMessage: { _ in + }, sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in @@ -155,8 +155,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, activateSwitchInline: { _, _, _ in }, openUrl: { _ in }, openExternalInstantPage: { _ in - }, shareCurrentLocation: { - }, shareAccountContact: { + }, shareCurrentLocation: { _ in + }, shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in @@ -203,7 +203,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, displaySwipeToReplyHint: { }, dismissReplyMarkupMessage: { _ in }, openMessagePollResults: { _, _ in - }, openPollCreation: { _ in + }, openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, displayPollSolution: { _, _ in }, displayPsa: { _, _ in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift index 80dd636401..b40f5419d0 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift @@ -828,8 +828,14 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { } func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, maxHeight: CGFloat, savedMusic: Bool?, transition: ContainedViewLayoutTransition) -> CGFloat { + let previousWidth = self.validLayout?.width self.validLayout = (width, leftInset, rightInset, bottomInset, maxHeight, savedMusic) + var transition = transition + if previousWidth == nil || previousWidth?.isZero == true { + transition = .immediate + } + let finalPanelHeight = OverlayAudioPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, maxHeight: maxHeight, isExpanded: self.isExpanded, savedMusic: savedMusic) let panelHeight = OverlayAudioPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, maxHeight: maxHeight, isExpanded: self.isExpanded, savedMusic: nil) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3e44de995f..6b701ebe86 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 @@ -22,6 +21,7 @@ import LocalMediaResources import OverlayStatusController import AlertUI import PresentationDataUtils +import OpenUserGeneratedUrl import LocationUI import AppLock import WallpaperBackgroundNode @@ -2244,15 +2244,15 @@ 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 openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, webpage: TelegramMediaWebpage?, concealed: Bool, forceConcealed: Bool, skipUrlAuth: Bool, skipConcealedAlert: Bool, forceDark: Bool, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise?, alertDisplayUpdated: ((ViewController?) -> Void)?, concealedAlertOption: OpenUserGeneratedUrlConcealedAlertOption?) -> Disposable { + return OpenUserGeneratedUrl.openUserGeneratedUrl(context: context, peerId: peerId, url: url, webpage: webpage, concealed: concealed, forceConcealed: forceConcealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, forceDark: forceDark, present: present, openResolved: openResolved, progress: progress, alertDisplayUpdated: alertDisplayUpdated, concealedAlertOption: concealedAlertOption) + } + 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) } @@ -2379,7 +2379,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _, _ in }, - sendMessage: { _ in }, + sendMessage: { _, _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, @@ -2394,8 +2394,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { openUrl: { _ in }, openExternalInstantPage: { _ in }, - shareCurrentLocation: {}, - shareAccountContact: {}, + shareCurrentLocation: { _ in }, + shareAccountContact: { _ in }, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, @@ -2474,7 +2474,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openMessagePollResults: { _, _ in }, - openPollCreation: { _ in + openPollCreation: { _, _ in }, openPollMedia: { _, _ in }, @@ -3719,16 +3719,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) + }) }) } @@ -3962,8 +3967,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return stickerMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed) } - public func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?) { - return avatarMediaPickerController(context: context, getSourceRect: getSourceRect, canDelete: canDelete, performDelete: performDelete, completion: completion, dismissed: dismissed) + public func makeAvatarMediaPickerScreen(context: AccountContext, peerType: PeerType, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?) { + return avatarMediaPickerController(context: context, peerType: peerType, getSourceRect: getSourceRect, canDelete: canDelete, performDelete: performDelete, completion: completion, dismissed: dismissed) } public func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController { @@ -4265,6 +4270,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: chatPeer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload, verifyAgeCompletion: verifyAgeCompletion) } + public func openJoinChatWebView(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, webView: JoinChatWebView) { + openJoinChatWebViewImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, webView: webView) + } + public func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal { return AffiliateProgramSetupScreen.content(context: context, peerId: peerId, mode: mode) } @@ -4306,8 +4315,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { return CocoonInfoScreen(context: context) } - public func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?) -> Void) -> ViewController { - return chatTextLinkEditController(context: context, updatedPresentationData: updatedPresentationData, text: text, link: link, apply: apply) + public func makeLinkEditController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, text: String, link: String?, apply: @escaping (String?, TelegramMediaWebpage?) -> Void) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + return chatTextLinkEditController(context: context, updatedPresentationData: updatedPresentationData, text: presentationData.strings.TextFormat_AddLinkText(text).string, link: link, apply: apply) } @available(iOS 13.0, *) diff --git a/submodules/TelegramUI/Sources/SharedWakeupManager.swift b/submodules/TelegramUI/Sources/SharedWakeupManager.swift index d89f341efa..97de6187fc 100644 --- a/submodules/TelegramUI/Sources/SharedWakeupManager.swift +++ b/submodules/TelegramUI/Sources/SharedWakeupManager.swift @@ -1133,7 +1133,9 @@ public final class SharedWakeupManager { } private func updateAccounts(hasTasks: Bool, endTaskAfterTransactionsComplete: UIBackgroundTaskIdentifier?) { - if self.inForeground || self.hasActiveAudioSession || self.isInBackgroundExtension || self.backgroundProcessingTaskId != nil || self.backgroundStoryProcessingTaskId != nil || (hasTasks && self.currentExternalCompletion != nil) || self.activeExplicitExtensionTimer != nil || self.silenceAudioRenderer != nil { + let hasBackgroundLocationTask = self.accountsAndTasks.contains(where: { $0.2.backgroundLocation }) + + if self.inForeground || self.hasActiveAudioSession || self.isInBackgroundExtension || self.backgroundProcessingTaskId != nil || self.backgroundStoryProcessingTaskId != nil || hasBackgroundLocationTask || (hasTasks && self.currentExternalCompletion != nil) || self.activeExplicitExtensionTimer != nil || self.silenceAudioRenderer != nil { Logger.shared.log("Wakeup", "enableBeginTransactions: true (active)") for (account, primary, tasks) in self.accountsAndTasks { @@ -1144,7 +1146,7 @@ public final class SharedWakeupManager { } else { account.shouldBeServiceTaskMaster.set(.single(.never)) } - account.shouldExplicitelyKeepWorkerConnections.set(.single(tasks.backgroundAudio || tasks.importantTasks.pendingStoryCount != 0 || tasks.importantTasks.pendingMessageCount != 0)) + account.shouldExplicitelyKeepWorkerConnections.set(.single(tasks.backgroundAudio || tasks.backgroundLocation || tasks.importantTasks.pendingStoryCount != 0 || tasks.importantTasks.pendingMessageCount != 0)) account.shouldKeepOnlinePresence.set(.single(primary && self.inForeground)) account.shouldKeepBackgroundDownloadConnections.set(.single(tasks.backgroundDownloads)) } diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 55d1c95193..8961fa49e0 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -111,8 +111,6 @@ public struct ApplicationSpecificItemCacheCollectionId { } private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { - case webSearchRecentQueries = 0 - case wallpaperSearchRecentQueries = 1 case localThemes = 3 case storyDrafts = 4 case storySources = 5 @@ -122,8 +120,6 @@ private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { } public struct ApplicationSpecificOrderedItemListCollectionId { - public static let webSearchRecentQueries = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.webSearchRecentQueries.rawValue) - public static let wallpaperSearchRecentQueries = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.wallpaperSearchRecentQueries.rawValue) public static let settingsSearchRecentItems = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.settingsSearchRecentItems.rawValue) public static let localThemes = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.localThemes.rawValue) public static let storyDrafts = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storyDrafts.rawValue) diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index e4502acd6d..61bfacc05c 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -447,12 +447,12 @@ public final class PeerChannelMemberCategoriesContextsManager { } } - public func join(engine: TelegramEngine, peerId: PeerId, hash: String?) -> Signal { + public func join(engine: TelegramEngine, peerId: PeerId, hash: String?) -> Signal { return engine.peers.joinChannel(peerId: peerId, hash: hash) |> deliverOnMainQueue |> beforeNext { [weak self] result in - if let strongSelf = self { - strongSelf.impl.with { impl in + if let self { + self.impl.with { impl in for (contextPeerId, context) in impl.contexts { if peerId == contextPeerId { switch result { @@ -468,7 +468,6 @@ public final class PeerChannelMemberCategoriesContextsManager { } } } - |> ignoreValues } public func addMember(engine: TelegramEngine, peerId: PeerId, memberId: PeerId) -> Signal { diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 998b55ee55..9de9c1b5b0 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -603,9 +603,14 @@ private func refreshTextMentions(text: NSString, initialAttributedText: NSAttrib } } -private func isTextUrlInnerCharacter(_ c: UnicodeScalar) -> Bool { - return alphanumericCharacters.contains(c) || c == " " as UnicodeScalar -} +private let textUrlEdgeCharacters = CharacterSet.alphanumerics + +private let textUrlCharacters: CharacterSet = { + var set = textUrlEdgeCharacters + set.insert(charactersIn: "@_") + set.formUnion(.whitespacesAndNewlines) + return set +}() private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributedString, attributedText: NSMutableAttributedString, fullRange: NSRange) { var textUrlRanges: [(NSRange, ChatTextInputTextUrlAttribute)] = [] @@ -623,7 +628,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed var validLower = range.lowerBound inner1: for i in range.lowerBound ..< range.upperBound { if let c = UnicodeScalar(text.character(at: i)) { - if isTextUrlInnerCharacter(c) { + if textUrlCharacters.contains(c) { validLower = i break inner1 } @@ -634,7 +639,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed var validUpper = range.upperBound inner2: for i in (validLower ..< range.upperBound).reversed() { if let c = UnicodeScalar(text.character(at: i)) { - if isTextUrlInnerCharacter(c) { + if textUrlCharacters.contains(c) { validUpper = i + 1 break inner2 } @@ -646,7 +651,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed let minLower = (i == 0) ? fullRange.lowerBound : textUrlRanges[i - 1].0.upperBound inner3: for i in (minLower ..< validLower).reversed() { if let c = UnicodeScalar(text.character(at: i)) { - if alphanumericCharacters.contains(c) { + if textUrlEdgeCharacters.contains(c) { validLower = i } else { break inner3 @@ -659,7 +664,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed let maxUpper = (i == textUrlRanges.count - 1) ? fullRange.upperBound : textUrlRanges[i + 1].0.lowerBound inner3: for i in validUpper ..< maxUpper { if let c = UnicodeScalar(text.character(at: i)) { - if alphanumericCharacters.contains(c) { + if textUrlEdgeCharacters.contains(c) { validUpper = i + 1 } else { break inner3 @@ -681,7 +686,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed var combine = true inner: for j in textUrlRanges[i].0.upperBound ..< textUrlRanges[i + 1].0.lowerBound { if let c = UnicodeScalar(text.character(at: j)) { - if isTextUrlInnerCharacter(c) { + if textUrlCharacters.contains(c) { } else { combine = false break inner diff --git a/submodules/TranslateUI/Sources/LanguageSelectionController.swift b/submodules/TranslateUI/Sources/LanguageSelectionController.swift index a9d2e4ea86..dcea89c7b1 100644 --- a/submodules/TranslateUI/Sources/LanguageSelectionController.swift +++ b/submodules/TranslateUI/Sources/LanguageSelectionController.swift @@ -153,7 +153,7 @@ public func languageSelectionController(context: AccountContext, forceTheme: Pre if let forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) } - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.Translate_Languages_Original, presentationData.strings.Translate_Languages_Translation], 1), leftNavigationButton: ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}), rightNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.Translate_Languages_Original, presentationData.strings.Translate_Languages_Translation], 1), leftNavigationButton: ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}), rightNavigationButton: ItemListNavigationButton(content: .icon(.done), style: .bold, enabled: true, action: { completion(state.fromLanguage, state.toLanguage) dismissImpl?() }), backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) diff --git a/submodules/TranslateUI/Sources/LocalizationListItem.swift b/submodules/TranslateUI/Sources/LocalizationListItem.swift index 07ba6eba56..a8ac41666d 100644 --- a/submodules/TranslateUI/Sources/LocalizationListItem.swift +++ b/submodules/TranslateUI/Sources/LocalizationListItem.swift @@ -383,7 +383,7 @@ class LocalizationListItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) if item.editing.editable, item.removeItem != nil { - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, iconColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor, textColor: item.presentationData.theme.list.itemSecondaryTextColor)])) } else { strongSelf.setRevealOptions((left: [], right: [])) } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 9766d60e4b..5a92356531 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -8,7 +8,15 @@ import TelegramUIPreferences import TelegramNotices import AccountContext -private let baseTelegramMePaths = ["telegram.me", "t.me", "telegram.dog"] +private let baseTelegramMePaths = [ + "telegram.me", + "t.me", "telegram.dog" +] +private let telegramWebShortLinkHosts = [ + "a.t.me", + "k.t.me", + "z.t.me" +] private let baseTelegraPhPaths = [ "telegra.ph/", "te.legra.ph/", @@ -18,6 +26,20 @@ private let baseTelegraPhPaths = [ "telegram.org/tour/" ] +public func isTelegramWebShortLink(_ url: String) -> Bool { + var urlWithScheme = url + if !urlWithScheme.contains("://") { + urlWithScheme = "https://\(urlWithScheme)" + } + guard let parsedUrl = URL(string: urlWithScheme), let host = parsedUrl.host?.lowercased() else { + return false + } + if let scheme = parsedUrl.scheme?.lowercased(), scheme != "http" && scheme != "https" { + return false + } + return telegramWebShortLinkHosts.contains(host) +} + extension ResolvedBotAdminRights { init?(_ string: String) { var rawValue: UInt32 = 0 @@ -1548,6 +1570,10 @@ public func resolveUrlImpl(context: AccountContext, peerId: EnginePeer.Id?, url: } } + if isTelegramWebShortLink(url) { + return .single(.result(.externalUrl(url))) + } + for basePath in baseTelegramMePaths { for scheme in schemes { let basePrefix = scheme + basePath + "/" diff --git a/submodules/DateSelectionUI/BUILD b/submodules/Weather/BUILD similarity index 51% rename from submodules/DateSelectionUI/BUILD rename to submodules/Weather/BUILD index be8c15b508..20aba18a37 100644 --- a/submodules/DateSelectionUI/BUILD +++ b/submodules/Weather/BUILD @@ -1,8 +1,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") swift_library( - name = "DateSelectionUI", - module_name = "DateSelectionUI", + name = "Weather", + module_name = "Weather", srcs = glob([ "Sources/**/*.swift", ]), @@ -10,13 +10,11 @@ swift_library( "-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", + "//submodules/DeviceAccess", + "//submodules/DeviceLocationManager", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramCore:TelegramCore", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift b/submodules/Weather/Sources/Weather.swift similarity index 76% rename from submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift rename to submodules/Weather/Sources/Weather.swift index f6f93ba274..e6f6f9360a 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/Weather.swift +++ b/submodules/Weather/Sources/Weather.swift @@ -2,17 +2,69 @@ import Foundation import CoreLocation import SwiftSignalKit import TelegramCore -import StickerPickerScreen import AccountContext import DeviceLocationManager import DeviceAccess -struct StoryWeather { - let emoji: String - let temperature: Double +public struct WeatherData { + public let emoji: String + public let temperature: Double + + public init(emoji: String, temperature: Double) { + self.emoji = emoji + self.temperature = temperature + } } -private func getWeatherData(context: AccountContext, location: CLLocationCoordinate2D) -> Signal { +public enum WeatherDataState { + case none + case notDetermined + case notAllowed + case notPreloaded + case fetching + case loaded(WeatherData) +} + +public func getWeatherData(context: AccountContext, load: Bool) -> Signal { + if !load { + return .single(.notPreloaded) + } + + guard let locationManager = context.sharedContext.locationManager else { + return .single(.none) + } + + return DeviceAccess.authorizationStatus(subject: .location(.send)) + |> mapToSignal { status in + switch status { + case .notDetermined: + return .single(.notDetermined) + case .denied, .restricted, .unreachable, .limited: + return .single(.notAllowed) + case .allowed: + return .single(.fetching) + |> then( + currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) + |> mapToSignal { location in + guard let location else { + return .single(.none) + } + + return requestWeatherData(context: context, location: location) + |> map { weather in + if let weather { + return .loaded(weather) + } else { + return .none + } + } + } + ) + } + } +} + +public func requestWeatherData(context: AccountContext, location: CLLocationCoordinate2D) -> Signal { let appConfiguration = context.currentAppConfiguration.with { $0 } let botConfiguration = WeatherBotConfiguration.with(appConfiguration: appConfiguration) @@ -20,7 +72,7 @@ private func getWeatherData(context: AccountContext, location: CLLocationCoordin return context.engine.peers.resolvePeerByName(name: botUsername, referrer: nil) |> mapToSignal { result -> Signal in guard case let .result(result) = result else { - return .complete() + return .single(nil) } return .single(result) } @@ -36,65 +88,18 @@ private func getWeatherData(context: AccountContext, location: CLLocationCoordin return .single(nil) } } - |> map { contextResult -> StoryWeather? in + |> map { contextResult -> WeatherData? in guard let contextResult, let result = contextResult.results.first, let emoji = result.title, let temperature = result.description.flatMap(Double.init) else { return nil } - return StoryWeather(emoji: emoji, temperature: temperature) + let effectiveEmoji = emojiFor(for: strippedEmoji(emoji), date: Date(), location: location) + return WeatherData(emoji: effectiveEmoji, temperature: temperature) } } else { return .single(nil) } } -func getWeather(context: AccountContext, load: Bool) -> Signal { - guard let locationManager = context.sharedContext.locationManager else { - return .single(.none) - } - - return DeviceAccess.authorizationStatus(subject: .location(.send)) - |> mapToSignal { status in - switch status { - case .notDetermined: - return .single(.notDetermined) - case .denied, .restricted, .unreachable, .limited: - return .single(.notAllowed) - case .allowed: - if load { - return .single(.fetching) - |> then( - currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) - |> mapToSignal { location in - if let location { - return getWeatherData(context: context, location: location) - |> mapToSignal { weather in - if let weather { - let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location) - if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first { - return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather( - emoji: effectiveEmoji, - emojiFile: match.file._parse(), - temperature: weather.temperature - ))) - } else { - return .single(.none) - } - } else { - return .single(.none) - } - } - } else { - return .single(.none) - } - } - ) - } else { - return .single(.notPreloaded) - } - } - } -} - private struct WeatherBotConfiguration { static var defaultValue: WeatherBotConfiguration { return WeatherBotConfiguration(botName: "izweatherbot") @@ -118,6 +123,16 @@ private struct WeatherBotConfiguration { private let J1970: Double = 2440588.0 private let moonEmojis = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑"] +private func strippedEmoji(_ emoji: String) -> String { + var string = "" + for scalar in emoji.unicodeScalars { + if scalar.value != 0xfe0f { + string.unicodeScalars.append(scalar) + } + } + return string +} + private func emojiFor(for emoji: String, date: Date, location: CLLocationCoordinate2D) -> String { var emoji = emoji if !"".isEmpty, ["☀️", "🌤️"].contains(emoji) && !isDay(latitude: location.latitude, longitude: location.longitude, dateTime: date) { diff --git a/submodules/WebSearchUI/BUILD b/submodules/WebSearchUI/BUILD deleted file mode 100644 index 7bde4f23ba..0000000000 --- a/submodules/WebSearchUI/BUILD +++ /dev/null @@ -1,39 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "WebSearchUI", - module_name = "WebSearchUI", - 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/LegacyComponents:LegacyComponents", - "//submodules/TelegramUIPreferences:TelegramUIPreferences", - "//submodules/MergeLists:MergeLists", - "//submodules/GalleryUI:GalleryUI", - "//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader", - "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", - "//submodules/CheckNode:CheckNode", - "//submodules/PhotoResources:PhotoResources", - "//submodules/SearchBarNode:SearchBarNode", - "//submodules/ItemListUI:ItemListUI", - "//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI", - "//submodules/SegmentedControlNode:SegmentedControlNode", - "//submodules/AppBundle:AppBundle", - "//submodules/PresentationDataUtils:PresentationDataUtils", - "//submodules/AttachmentUI:AttachmentUI", - "//submodules/RadialStatusNode:RadialStatusNode", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift deleted file mode 100644 index b26bbdb913..0000000000 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchEditor.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation -import UIKit -import LegacyComponents -import SwiftSignalKit -import TelegramCore -import SSignalKit -import Display -import TelegramPresentationData -import AccountContext -import LegacyUI -import LegacyMediaPickerUI - -func presentLegacyWebSearchEditor(context: AccountContext, theme: PresentationTheme, result: ChatContextResult, initialLayout: ContainerViewLayout?, updateHiddenMedia: @escaping (String?) -> Void, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (UIImage) -> Void, present: @escaping (ViewController, Any?) -> Void) { - guard let item = legacyWebSearchItem(engine: context.engine, result: result) else { - return - } - - var screenImage: Signal = .single(nil) - if let resource = item.thumbnailResource { - screenImage = context.engine.resources.data(resource: EngineMediaResource(resource), attemptSynchronously: true) - |> map { maybeData -> UIImage? in - if maybeData.isComplete { - if let loadedData = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []), let image = UIImage(data: loadedData) { - return image - } - } - return nil - } - } - - let _ = (screenImage - |> take(1) - |> deliverOnMainQueue).start(next: { screenImage in - let legacyController = LegacyController(presentation: .custom, theme: theme, initialLayout: initialLayout) - legacyController.statusBar.statusBarStyle = theme.rootController.statusBarStyle.style - - let paintStickersContext = LegacyPaintStickersContext(context: context) - - let controller = TGPhotoEditorController(context: legacyController.context, item: item, intent: TGPhotoEditorControllerAvatarIntent, adjustments: nil, caption: nil, screenImage: screenImage ?? UIImage(), availableTabs: TGPhotoEditorController.defaultTabs(forAvatarIntent: true), selectedTab: .cropTab)! - controller.stickersContext = paintStickersContext - legacyController.bind(controller: controller) - - controller.editingContext = TGMediaEditingContext() - controller.didFinishEditing = { [weak controller] _, result, _, hasChanges, commit in - if !hasChanges { - return - } - if let result = result { - completed(result) - } - commit?() - controller?.dismiss(animated: true) - } - controller.requestThumbnailImage = { _ -> SSignal in - return item.thumbnailImageSignal() - } - controller.requestOriginalScreenSizeImage = { _, position -> SSignal in - return item.screenImageSignal(position) - } - controller.requestOriginalFullSizeImage = { _, position -> SSignal in - return item.originalImageSignal(position) - } - - let fromView = transitionView(result)! - let transition = TGMediaAvatarEditorTransition(controller: controller, from: fromView)! - transition.transitionHostView = transitionHostView() - transition.referenceFrame = { - return fromView.frame - } - transition.referenceImageSize = { - return item.dimensions - } - transition.referenceScreenImageSignal = { - return item.screenImageSignal(0.0) - } - transition.imageReady = { - updateHiddenMedia(result.id) - } - - controller.beginCustomTransitionOut = { [weak legacyController] outFrame, outView, completion in - transition.outReferenceFrame = outFrame - transition.repView = outView - transition.dismiss(animated: true, completion: { - updateHiddenMedia(nil) - if let completion = completion { - DispatchQueue.main.async { - completion() - } - } - legacyController?.dismiss() - }) - } - - present(legacyController, nil) - transition.present(animated: true) - }) -} diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift deleted file mode 100644 index ac7aee4ab9..0000000000 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift +++ /dev/null @@ -1,474 +0,0 @@ -import Foundation -import UIKit -import LegacyComponents -import SwiftSignalKit -import TelegramCore -import SSignalKit -import UIKit -import Display -import TelegramPresentationData -import AccountContext -import PhotoResources -import LegacyUI -import LegacyMediaPickerUI - -class LegacyWebSearchItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem { - var isVideo: Bool { - return false - } - - var uniqueIdentifier: String! { - return self.result.id - } - - let result: ChatContextResult - private(set) var thumbnailResource: TelegramMediaResource? - private(set) var imageResource: TelegramMediaResource? - let dimensions: CGSize - let thumbnailImage: Signal - let originalImage: Signal - let progress: Signal - - init(result: ChatContextResult) { - self.result = result - self.dimensions = CGSize() - self.thumbnailImage = .complete() - self.originalImage = .complete() - self.progress = .complete() - } - - init(result: ChatContextResult, thumbnailResource: TelegramMediaResource?, imageResource: TelegramMediaResource?, dimensions: CGSize, thumbnailImage: Signal, originalImage: Signal, progress: Signal) { - self.result = result - self.thumbnailResource = thumbnailResource - self.imageResource = imageResource - self.dimensions = dimensions - self.thumbnailImage = thumbnailImage - self.originalImage = originalImage - self.progress = progress - } - - var originalSize: CGSize { - return self.dimensions - } - - func thumbnailImageSignal() -> SSignal! { - return SSignal(generator: { subscriber -> SDisposable? in - let disposable = self.thumbnailImage.start(next: { image in - subscriber.putNext(image) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - } - - func screenImageAndProgressSignal() -> SSignal { - return SSignal { subscriber in - let imageDisposable = self.originalImage.start(next: { image in - if !image.degraded() { - subscriber.putNext(1.0) - } - subscriber.putNext(image) - if !image.degraded() { - subscriber.putCompletion() - } - }) - - let progressDisposable = (self.progress - |> deliverOnMainQueue).start(next: { next in - subscriber.putNext(next) - }) - - return SBlockDisposable { - imageDisposable.dispose() - progressDisposable.dispose() - } - } - } - - func screenImageSignal(_ position: TimeInterval) -> SSignal! { - return self.originalImageSignal(position) - } - - func originalImageSignal(_ position: TimeInterval) -> SSignal! { - return SSignal(generator: { subscriber -> SDisposable? in - let disposable = self.originalImage.start(next: { image in - subscriber.putNext(image) - if !image.degraded() { - subscriber.putCompletion() - } - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - } -} - -private class LegacyWebSearchGalleryItem: TGModernGalleryImageItem, TGModernGalleryEditableItem, TGModernGallerySelectableItem { - var selectionContext: TGMediaSelectionContext! - var editingContext: TGMediaEditingContext! - var stickersContext: TGPhotoPaintStickersContext! - let item: LegacyWebSearchItem - - init(item: LegacyWebSearchItem) { - self.item = item - super.init() - } - - func editableMediaItem() -> TGMediaEditableItem! { - return self.item - } - - func selectableMediaItem() -> TGMediaSelectableItem! { - return self.item - } - - func toolbarTabs() -> TGPhotoEditorTab { - return [.cropTab, .paintTab, .toolsTab] - } - - func uniqueId() -> String! { - return self.item.uniqueIdentifier - } - - override func viewClass() -> AnyClass! { - return LegacyWebSearchGalleryItemView.self - } - - override func isEqual(_ object: Any?) -> Bool { - if let item = object as? LegacyWebSearchGalleryItem { - return item.item.result.id == self.item.result.id - } - return false - } -} - -private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGModernGalleryEditableItemView { - private let readyForTransition = SVariable() - - @objc func setHiddenAsBeingEdited(_ hidden: Bool) { - self.imageView.isHidden = hidden - } - - @objc func singleTap() { - if let item = item as? LegacyWebSearchGalleryItem, let selectionContext = item.selectionContext { - selectionContext.toggleItemSelection(item.selectableMediaItem(), success: nil) - } - } - - override func readyForTransitionIn() -> SSignal! { - return self.readyForTransition.signal().take(1) - } - - override func setItem(_ item: TGModernGalleryItem!, synchronously: Bool) { - if let item = item as? LegacyWebSearchGalleryItem { - self._setItem(item) - self.imageSize = TGFitSize(item.editableMediaItem().originalSize!, CGSize(width: 1600, height: 1600)) - - let signal = item.editingContext.imageSignal(for: item.editableMediaItem())?.map(toSignal: { result -> SSignal in - if let image = result as? UIImage { - return SSignal.single(image) - } else if result == nil, let mediaItem = item.editableMediaItem() as? LegacyWebSearchItem { - return mediaItem.screenImageAndProgressSignal() - } else { - return SSignal.complete() - } - }) - - self.imageView.setSignal(signal?.deliver(on: SQueue.main()).afterNext({ [weak self] next in - if let strongSelf = self, let image = next as? UIImage { - strongSelf.imageSize = image.size - strongSelf.reset() - strongSelf.readyForTransition.set(SSignal.single(true)) - } - })) - - self.reset() - } else { - self.imageView.setSignal(nil) - super.setItem(item, synchronously: synchronously) - } - } - - override func contentView() -> UIView! { - return self.imageView - } - - override func transitionContentView() -> UIView! { - return self.contentView() - } - - override func transitionViewContentRect() -> CGRect { - let contentView = self.transitionContentView()! - return contentView.convert(contentView.bounds, to: self.transitionView()) - } -} - -func legacyWebSearchItem(engine: TelegramEngine, result: ChatContextResult) -> LegacyWebSearchItem? { - var thumbnailDimensions: CGSize? - var thumbnailResource: TelegramMediaResource? - var imageResource: TelegramMediaResource? - var imageDimensions = CGSize() - var immediateThumbnailData: Data? - - let thumbnailSignal: Signal - let originalSignal: Signal - - switch result { - case let .externalReference(externalReference): - if let content = externalReference.content { - imageResource = content.resource - } - if let thumbnail = externalReference.thumbnail { - thumbnailResource = thumbnail.resource - thumbnailDimensions = thumbnail.dimensions?.cgSize - } - if let dimensions = externalReference.content?.dimensions { - imageDimensions = dimensions.cgSize - } - case let .internalReference(internalReference): - immediateThumbnailData = internalReference.image?.immediateThumbnailData - if let image = internalReference.image { - if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) { - imageDimensions = imageRepresentation.dimensions.cgSize - imageResource = imageRepresentation.resource - } - if let thumbnailRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 200, height: 100)) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } - } - - if let imageResource = imageResource { - let progressSignal = engine.resources.status(resource: EngineMediaResource(imageResource)) - |> map { status -> Float in - switch status { - case .Local: - return 1.0 - case .Remote, .Paused: - return 0.027 - case let .Fetching(_, progress): - return max(progress, 0.1) - } - } - - var representations: [TelegramMediaImageRepresentation] = [] - if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) - } - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) - let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - thumbnailSignal = chatMessagePhotoDatas(postbox: engine.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: false) - |> mapToSignal { value -> Signal in - let thumbnailData = value._0 - if let data = thumbnailData, let image = UIImage(data: data) { - return .single(image) - } else { - return .complete() - } - } - originalSignal = chatMessagePhotoDatas(postbox: engine.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true) - |> mapToSignal { value -> Signal in - let thumbnailData = value._0 - let fullSizeData = value._1 - let fullSizeComplete = value._3 - - if fullSizeComplete, let data = fullSizeData, let image = UIImage(data: data) { - return .single(image) - } else if let data = thumbnailData, let image = UIImage(data: data) { - image.setDegraded(true) - return .single(image) - } else { - return .complete() - } - } - - return LegacyWebSearchItem(result: result, thumbnailResource: thumbnailResource, imageResource: imageResource, dimensions: imageDimensions, thumbnailImage: thumbnailSignal, originalImage: originalSignal, progress: progressSignal) - } else { - return nil - } -} - -private func galleryItems(engine: TelegramEngine, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext) -> ([TGModernGalleryItem], TGModernGalleryItem?) { - var focusItem: TGModernGalleryItem? - var galleryItems: [TGModernGalleryItem] = [] - for result in results { - if let item = legacyWebSearchItem(engine: engine, result: result) { - let galleryItem = LegacyWebSearchGalleryItem(item: item) - galleryItem.selectionContext = selectionContext - galleryItem.editingContext = editingContext - if result.id == current.id { - focusItem = galleryItem - } - galleryItems.append(galleryItem) - } - } - return (galleryItems, focusItem) -} - -func presentLegacyWebSearchGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, presentationData: PresentationData, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (ChatContextResult) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: (ViewController, Any?) -> Void) { - let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil) - legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style - - let recipientName: String? - if let threadTitle { - recipientName = threadTitle - } else { - if peer?.id == context.account.peerId { - recipientName = presentationData.strings.DialogList_SavedMessages - } else { - recipientName = peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - } - } - - let paintStickersContext = LegacyPaintStickersContext(context: context) - paintStickersContext.captionPanelView = { - return getCaptionPanelView() - } - - let controller = TGModernGalleryController(context: legacyController.context)! - controller.asyncTransitionIn = true - legacyController.bind(controller: controller) - - let (items, focusItem) = galleryItems(engine: context.engine, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext) - - let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName, isScheduledMessages: false, hasCoverButton: false)! - model.stickersContext = paintStickersContext - controller.model = model - model.controller = controller - model.useGalleryImageAsEditableItemImage = true - model.storeOriginalImageForItem = { item, image in - editingContext.setOriginalImage(image, for: item, synchronous: false) - } - model.willFinishEditingItem = { item, adjustments, representation, hasChanges in - if hasChanges { - editingContext.setAdjustments(adjustments, for: item) - } - editingContext.setTemporaryRep(representation, for: item) - if let selectionContext = selectionContext, adjustments != nil, let item = item as? TGMediaSelectableItem { - selectionContext.setItem(item, selected: true) - } - } - model.didFinishEditingItem = { item, adjustments, result, thumbnail in - editingContext.setImage(result, thumbnailImage: thumbnail, for: item, synchronous: true) - } - model.saveItemCaption = { item, caption in - editingContext.setCaption(caption, for: item) - if let selectionContext = selectionContext, let caption = caption, caption.length > 0, let item = item as? TGMediaSelectableItem { - selectionContext.setItem(item, selected: true) - } - } - if let selectionContext = selectionContext { - model.interfaceView.updateSelectionInterface(selectionContext.count(), counterVisible: selectionContext.count() > 0, animated: false) - } - model.interfaceView.donePressed = { item in - if let item = item as? LegacyWebSearchGalleryItem { - controller.dismissWhenReady(animated: true) - completed(item.item.result) - } - } - controller.transitionHost = { - return transitionHostView() - } - var transitionedIn = false - controller.itemFocused = { item in - if let item = item as? LegacyWebSearchGalleryItem, transitionedIn { - updateHiddenMedia(item.item.result.id) - } - } - controller.beginTransitionIn = { item, _ in - if let item = item as? LegacyWebSearchGalleryItem { - return transitionView(item.item.result) - } else { - return nil - } - } - controller.startedTransitionIn = { - transitionedIn = true - updateHiddenMedia(current.id) - } - controller.beginTransitionOut = { item, _ in - if let item = item as? LegacyWebSearchGalleryItem { - return transitionView(item.item.result) - } else { - return nil - } - } - controller.completedTransitionOut = { [weak legacyController] in - updateHiddenMedia(nil) - legacyController?.dismiss() - } - present(legacyController, nil) -} - -public func legacyEnqueueWebSearchMessages(_ selectionState: TGMediaSelectionContext, _ editingState: TGMediaEditingContext, enqueueChatContextResult: (ChatContextResult) -> Void, enqueueMediaMessages: ([Any]) -> Void) -{ - var results: [ChatContextResult] = [] - for item in selectionState.selectedItems() { - if let item = item as? LegacyWebSearchItem { - results.append(item.result) - } - } - - if !results.isEmpty { - var signals: [Any] = [] - for result in results { - let editableItem = LegacyWebSearchItem(result: result) - if let adjustments = editingState.adjustments(for: editableItem) { - let animated = adjustments.paintingData?.hasAnimation ?? false - if let imageSignal = editingState.imageSignal(for: editableItem) { - let signal = imageSignal.map { image -> Any in - if let image = image as? UIImage { - var dict: [AnyHashable: Any] = [ - "type": "editedPhoto", - "image": image - ] - - if animated { - dict["isAnimation"] = true - if let photoEditorValues = adjustments as? PGPhotoEditorValues { - dict["adjustments"] = TGVideoEditAdjustments(photoEditorValues: photoEditorValues, preset: TGMediaVideoConversionPresetAnimation) - } - - let filePath = NSTemporaryDirectory().appending("/gifvideo_\(arc4random()).jpg") - let data = image.jpegData(compressionQuality: 0.8) - if let data = data { - let _ = try? data.write(to: URL(fileURLWithPath: filePath), options: []) - } - dict["url"] = NSURL(fileURLWithPath: filePath) - - if adjustments.cropApplied(forAvatar: false) || adjustments.hasPainting() || adjustments.toolsApplied() { - var paintingImage: UIImage? = adjustments.paintingData?.stillImage - if paintingImage == nil { - paintingImage = adjustments.paintingData?.image - } - - let thumbnailImage = TGPhotoEditorVideoExtCrop(image, paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(image.size, CGSize(width: 512.0, height: 512.0)), adjustments.originalSize, true, true, true, false) - if let thumbnailImage = thumbnailImage { - dict["previewImage"] = thumbnailImage - } - } - } - - return legacyAssetPickerItemGenerator()(dict, nil, nil, nil) as Any - } else { - return SSignal.complete() - } - } - signals.append(signal as Any) - } - } else { - enqueueChatContextResult(result) - } - } - - if !signals.isEmpty { - enqueueMediaMessages(signals) - } - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift b/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift deleted file mode 100644 index c52090e332..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchBadgeNode.swift +++ /dev/null @@ -1,96 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramPresentationData - -final class WebSearchBadgeNode: ASDisplayNode { - private var fillColor: UIColor - private var strokeColor: UIColor - private var textColor: UIColor - - private let textNode: ASTextNode - private let backgroundNode: ASImageNode - - private let font: UIFont = Font.with(size: 17.0, design: .round, weight: .bold) - - var text: String = "" { - didSet { - self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) - self.invalidateCalculatedLayout() - } - } - - convenience init(theme: PresentationTheme) { - self.init(fillColor: theme.list.itemCheckColors.fillColor, strokeColor: theme.list.itemCheckColors.fillColor, textColor: theme.list.itemCheckColors.foregroundColor) - } - - init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { - self.fillColor = fillColor - self.strokeColor = strokeColor - self.textColor = textColor - - self.textNode = ASTextNode() - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = false - - self.backgroundNode = ASImageNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 22.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) - - super.init() - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textNode) - } - - func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { - self.fillColor = fillColor - self.strokeColor = strokeColor - self.textColor = textColor - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 22.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) - self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) - } - - func animateBump(incremented: Bool) { - if incremented { - let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) - firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.2) - firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.2, completion: { finished in - if finished { - let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) - secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0) - secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0) - } - }) - } else { - let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) - firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 0.8) - firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 0.8, completion: { finished in - if finished { - let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) - secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0) - secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0) - } - }) - } - } - - func animateOut() { - let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue - self.backgroundNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil) - self.textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil) - } - - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - let badgeSize = self.textNode.measure(constrainedSize) - let backgroundSize = CGSize(width: max(22.0, badgeSize.width + 12.0), height: 22.0) - let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) - self.backgroundNode.frame = backgroundFrame - self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0) - UIScreenPixel), size: badgeSize) - - return backgroundSize - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift deleted file mode 100644 index d82b94462d..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ /dev/null @@ -1,608 +0,0 @@ -import Foundation -import UIKit -import SwiftSignalKit -import Display -import AsyncDisplayKit -import TelegramCore -import LegacyComponents -import TelegramUIPreferences -import TelegramPresentationData -import AccountContext -import AttachmentUI - -public enum WebSearchMode { - case media - case avatar -} - -public enum WebSearchControllerMode { - case media(attachment: Bool, completion: (ChatContextResultCollection, TGMediaSelectionContext, TGMediaEditingContext, Bool, Int32?) -> Void) - case editor(completion: (UIImage) -> Void) - case avatar(initialQuery: String?, completion: (UIImage) -> Void) - - var mode: WebSearchMode { - switch self { - case .media, .editor: - return .media - case .avatar: - return .avatar - } - } -} - -final class WebSearchControllerInteraction { - let openResult: (ChatContextResult) -> Void - let setSearchQuery: (String) -> Void - let deleteRecentQuery: (String) -> Void - let toggleSelection: (ChatContextResult, Bool) -> Bool - let sendSelected: (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?) -> Void - let schedule: (ChatSendMessageActionSheetController.SendParameters?) -> Void - let avatarCompleted: (UIImage) -> Void - let selectionState: TGMediaSelectionContext? - let editingState: TGMediaEditingContext - var hiddenMediaId: String? - - init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping (ChatContextResult, Bool) -> Bool, sendSelected: @escaping (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, avatarCompleted: @escaping (UIImage) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { - self.openResult = openResult - self.setSearchQuery = setSearchQuery - self.deleteRecentQuery = deleteRecentQuery - self.toggleSelection = toggleSelection - self.sendSelected = sendSelected - self.schedule = schedule - self.avatarCompleted = avatarCompleted - self.selectionState = selectionState - self.editingState = editingState - } -} - -private func selectionChangedSignal(selectionState: TGMediaSelectionContext) -> Signal { - return Signal { subscriber in - let disposable = selectionState.selectionChangedSignal()?.start(next: { next in - subscriber.putNext(Void()) - }, completed: {}) - return ActionDisposable { - disposable?.dispose() - } - } -} - -public struct WebSearchConfiguration: Equatable { - public let gifProvider: String? - - public init(appConfiguration: AppConfiguration) { - var gifProvider: String? = nil - if let data = appConfiguration.data, let value = data["gif_search_branding"] as? String { - gifProvider = value - } - self.gifProvider = gifProvider - } -} - -public final class WebSearchController: ViewController { - private var validLayout: ContainerViewLayout? - - private let context: AccountContext - let mode: WebSearchControllerMode - private let peer: EnginePeer? - private let chatLocation: ChatLocation? - private let configuration: EngineConfiguration.SearchBots - private let activateOnDisplay: Bool - - private var controllerNode: WebSearchControllerNode { - return self.displayNode as! WebSearchControllerNode - } - - private var _ready = Promise() - override public var ready: Promise { - return self._ready - } - - private var didPlayPresentationAnimation = false - - private var controllerInteraction: WebSearchControllerInteraction? - private var interfaceState: WebSearchInterfaceState - private let interfaceStatePromise = ValuePromise() - - private var disposable: Disposable? - private let resultsDisposable = MetaDisposable() - private var selectionDisposable: Disposable? - - private var navigationContentNode: WebSearchNavigationContentNode? - - public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } { - didSet { - self.controllerNode.getCaptionPanelView = self.getCaptionPanelView - } - } - - public var presentSchedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in } - - public var dismissed: () -> Void = { } - - public var searchingUpdated: (Bool) -> Void = { _ in } - - public var attemptItemSelection: (ChatContextResult) -> Bool = { _ in return true } - - private var searchQueryPromise = ValuePromise() - private var searchQueryDisposable: Disposable? - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: EngineConfiguration.SearchBots, mode: WebSearchControllerMode, activateOnDisplay: Bool = true) { - self.context = context - self.mode = mode - self.peer = peer - self.chatLocation = chatLocation - self.configuration = configuration - self.activateOnDisplay = activateOnDisplay - - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - self.interfaceState = WebSearchInterfaceState(presentationData: presentationData) - - var searchQuery: String? - if case let .avatar(initialQuery, _) = mode, let query = initialQuery { - searchQuery = query - self.interfaceState = self.interfaceState.withUpdatedQuery(query) - } - - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme).withUpdatedSeparatorColor(presentationData.theme.list.plainBackgroundColor).withUpdatedBackgroundColor(presentationData.theme.list.plainBackgroundColor), strings: NavigationBarStrings(presentationStrings: presentationData.strings))) - self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style - - self.scrollToTop = { [weak self] in - if let strongSelf = self { - strongSelf.controllerNode.scrollToTop(animated: true) - } - } - - let settings = self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webSearchSettings]) - |> map { sharedData -> WebSearchSettings in - if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.webSearchSettings]?.get(WebSearchSettings.self) { - return current - } else { - return WebSearchSettings.defaultSettings - } - } - - let gifProvider = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) - |> map { preferencesEntry -> String? in - guard let appConfiguration = preferencesEntry?.get(AppConfiguration.self) else { - return nil - } - let configuration = WebSearchConfiguration(appConfiguration: appConfiguration) - return configuration.gifProvider - } - |> distinctUntilChanged - - self.disposable = ((combineLatest(settings, (updatedPresentationData?.signal ?? context.sharedContext.presentationData), gifProvider)) - |> deliverOnMainQueue).start(next: { [weak self] settings, presentationData, gifProvider in - guard let strongSelf = self else { - return - } - strongSelf.updateInterfaceState { current -> WebSearchInterfaceState in - var updated = current - if case .media = mode, current.state?.scope != settings.scope { - updated = updated.withUpdatedScope(settings.scope) - } - if current.presentationData !== presentationData { - updated = updated.withUpdatedPresentationData(presentationData) - } - if current.gifProvider != gifProvider { - updated = updated.withUpdatedGifProvider(gifProvider) - } - return updated - } - }) - - var attachment = false - if case let .media(attachmentValue, _) = mode { - attachment = attachmentValue - } else if case .editor = mode { - attachment = true - } - let navigationContentNode = WebSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, attachment: attachment) - self.navigationContentNode = navigationContentNode - navigationContentNode.setQueryUpdated { [weak self] query in - if let strongSelf = self, strongSelf.isNodeLoaded { - strongSelf.searchQueryPromise.set(query) - strongSelf.searchingUpdated(!query.isEmpty) - } - } - navigationContentNode.cancel = { [weak self] in - if let strongSelf = self { - strongSelf.cancel() - } - } - self.navigationBar?.setContentNode(navigationContentNode, animated: false) - if let query = searchQuery { - navigationContentNode.setQuery(query) - } - - let selectionState: TGMediaSelectionContext? - switch self.mode { - case .media: - selectionState = TGMediaSelectionContext() - case .avatar: - selectionState = nil - case .editor: - selectionState = nil - } - let editingState = TGMediaEditingContext() - self.controllerInteraction = WebSearchControllerInteraction(openResult: { [weak self] result in - if let strongSelf = self { - strongSelf.controllerNode.openResult(currentResult: result, present: { [weak self] viewController, arguments in - if let strongSelf = self { - strongSelf.present(viewController, in: .window(.root), with: arguments, blockInteraction: true) - } - }) - } - }, setSearchQuery: { [weak self] query in - if let strongSelf = self { - strongSelf.navigationContentNode?.setQuery(query) - strongSelf.updateSearchQuery(query) - strongSelf.navigationContentNode?.deactivate() - } - }, deleteRecentQuery: { [weak self] query in - if let strongSelf = self { - let _ = removeRecentWebSearchQuery(engine: strongSelf.context.engine, string: query).start() - } - }, toggleSelection: { [weak self] result, value in - if let strongSelf = self { - if !strongSelf.attemptItemSelection(result) { - return false - } - let item = LegacyWebSearchItem(result: result) - strongSelf.controllerInteraction?.selectionState?.setItem(item, selected: value) - return true - } else { - return false - } - }, sendSelected: { [weak self] current, silently, scheduleTime, messageEffect in - if let selectionState = selectionState, let results = self?.controllerNode.currentExternalResults { - if let current = current { - let currentItem = LegacyWebSearchItem(result: current) - selectionState.setItem(currentItem, selected: true) - } - if case let .media(_, sendSelected) = mode { - sendSelected(results, selectionState, editingState, silently, scheduleTime) - } - } - }, schedule: { [weak self] messageEffect in - if let strongSelf = self { - strongSelf.presentSchedulePicker(false, { [weak self] time, silentPosting in - self?.controllerInteraction?.sendSelected(nil, silentPosting, time, nil) - }) - } - }, avatarCompleted: { result in - if case let .avatar(_, avatarCompleted) = mode { - avatarCompleted(result) - } - }, selectionState: selectionState, editingState: editingState) - - selectionState?.attemptSelectingItem = { [weak self] item in - guard let self else { - return false - } - - if let item = item as? LegacyWebSearchItem { - return self.attemptItemSelection(item.result) - } - - return true - } - - if let selectionState = selectionState { - self.selectionDisposable = (selectionChangedSignal(selectionState: selectionState) - |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self { - strongSelf.controllerNode.updateSelectionState(animated: true) - } - }) - } - - let throttledSearchQuery = self.searchQueryPromise.get() - |> mapToSignal { query -> Signal in - if !query.isEmpty { - return (.complete() |> delay(1.0, queue: Queue.mainQueue())) - |> then(.single(query)) - } else { - return .single(query) - } - } - - self.searchQueryDisposable = (throttledSearchQuery - |> deliverOnMainQueue).start(next: { [weak self] query in - if let self { - self.updateSearchQuery(query) - } - }) - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.disposable?.dispose() - self.resultsDisposable.dispose() - self.selectionDisposable?.dispose() - self.searchQueryDisposable?.dispose() - } - - public func cancel() { - self.controllerNode.dismissInput?() - self.controllerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in - self?.dismissed() - self?.dismiss() - }) - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { - self.didPlayPresentationAnimation = true - if case .modalSheet = presentationArguments.presentationAnimation { - self.controllerNode.animateIn() - } - } - } - - private var didActivateSearch = false - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - var select = false - if case let .avatar(initialQuery, _) = self.mode, let _ = initialQuery { - select = true - } - if case let .media(attachment, _) = self.mode, attachment && !self.didPlayPresentationAnimation { - self.didPlayPresentationAnimation = true - self.controllerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } else if case .editor = self.mode, !self.didPlayPresentationAnimation { - self.didPlayPresentationAnimation = true - self.controllerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - if !self.didActivateSearch && self.activateOnDisplay { - self.didActivateSearch = true - self.navigationContentNode?.activate(select: select) - } - } - - override public func loadDisplayNode() { - var attachment: Bool = false - if case let .media(attachmentValue, _) = self.mode, attachmentValue { - attachment = true - } - self.displayNode = WebSearchControllerNode(controller: self, context: self.context, presentationData: self.interfaceState.presentationData, controllerInteraction: self.controllerInteraction!, peer: self.peer, chatLocation: self.chatLocation, mode: self.mode.mode, attachment: attachment) - self.controllerNode.requestUpdateInterfaceState = { [weak self] animated, f in - if let strongSelf = self { - strongSelf.updateInterfaceState(f) - } - } - self.controllerNode.cancel = { [weak self] in - if let strongSelf = self { - strongSelf.dismiss() - } - } - self.controllerNode.dismissInput = { [weak self] in - if let strongSelf = self { - strongSelf.navigationContentNode?.deactivate() - } - } - self.controllerNode.updateInterfaceState(self.interfaceState, animated: false) - - self._ready.set(.single(true)) - self.displayNodeDidLoad() - } - - private func updateInterfaceState(animated: Bool = true, _ f: (WebSearchInterfaceState) -> WebSearchInterfaceState) { - let previousInterfaceState = self.interfaceState - let previousTheme = self.interfaceState.presentationData.theme - let previousStrings = self.interfaceState.presentationData.theme - let previousGifProvider = self.interfaceState.gifProvider - - let updatedInterfaceState = f(self.interfaceState) - self.interfaceState = updatedInterfaceState - self.interfaceStatePromise.set(updatedInterfaceState) - - if self.isNodeLoaded { - if previousTheme !== updatedInterfaceState.presentationData.theme || previousStrings !== updatedInterfaceState.presentationData.strings || previousGifProvider != updatedInterfaceState.gifProvider { - self.controllerNode.updatePresentationData(theme: updatedInterfaceState.presentationData.theme, strings: updatedInterfaceState.presentationData.strings) - } - if previousInterfaceState != self.interfaceState { - self.controllerNode.updateInterfaceState(self.interfaceState, animated: animated) - } - } - } - - private func updateSearchQuery(_ query: String) { - let scope: Signal - switch self.mode { - case .media: - scope = self.interfaceStatePromise.get() - |> map { state -> WebSearchScope? in - return state.state?.scope - } - |> distinctUntilChanged - case .avatar, .editor: - scope = .single(.images) - } - - self.updateInterfaceState { $0.withUpdatedQuery(query) } - - let scopes: [WebSearchScope: Promise<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool)>] = [.images: Promise(initializeOnFirstAccess: self.signalForQuery(query, scope: .images) - |> mapToSignal { result -> Signal<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool), NoError> in - return .single((result, false)) - |> then(.single((result, true))) - }), .gifs: Promise(initializeOnFirstAccess: self.signalForQuery(query, scope: .gifs) - |> mapToSignal { result -> Signal<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool), NoError> in - return .single((result, false)) - |> then(.single((result, true))) - })] - - var results = scope - |> mapToSignal { scope -> (Signal<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool), NoError>) in - if let scope = scope, let scopeResults = scopes[scope] { - return scopeResults.get() - } else { - return .complete() - } - } - - if query.isEmpty { - results = .single(({ _ in return nil}, false)) - self.navigationContentNode?.setActivity(false) - } - - let previousResults = Atomic<(ChatContextResultCollection, Bool)?>(value: nil) - self.resultsDisposable.set((results - |> deliverOnMainQueue).start(next: { [weak self] result, immediate in - if let strongSelf = self { - if let result = result(nil), case let .contextRequestResult(_, results) = result { - if let results = results { - let previous = previousResults.swap((results, immediate)) - if let previous = previous, previous.0.queryId == results.queryId && !previous.1 { - } else { - strongSelf.controllerNode.updateResults(results, immediate: immediate) - } - } - } else { - strongSelf.controllerNode.updateResults(nil) - } - } - })) - } - - private func signalForQuery(_ query: String, scope: WebSearchScope) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> { - let delayRequest = true - let signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ _ in return .contextRequestResult(nil, nil) }) - - let peerId = self.peer?.id ?? self.context.account.peerId - - let botName: String? - switch scope { - case .images: - botName = self.configuration.imageBotUsername - case .gifs: - botName = self.configuration.gifBotUsername - } - guard let name = botName else { - return .single({ _ in return .contextRequestResult(nil, nil) }) - } - - let context = self.context - let contextBot = self.context.engine.peers.resolvePeerByName(name: name, referrer: nil) - |> mapToSignal { result -> Signal in - guard case let .result(result) = result else { - return .complete() - } - return .single(result) - } - |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in - if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let results = requestContextResults(engine: context.engine, botId: user.id, query: query, peerId: peerId, limit: 64) - |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - return { _ in - return .contextRequestResult(.user(user), results?.results) - } - } - - let botResult: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ previousResult in - var passthroughPreviousResult: ChatContextResultCollection? - if let previousResult = previousResult { - if case let .contextRequestResult(previousUser, previousResults) = previousResult { - if previousUser?.id == user.id { - passthroughPreviousResult = previousResults - } - } - } - return .contextRequestResult(nil, passthroughPreviousResult) - }) - - let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> - if delayRequest { - maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue()) - } else { - maybeDelayedContextResults = results - } - - return botResult |> then(maybeDelayedContextResults) - } else { - return .single({ _ in return nil }) - } - } - return (signal |> then(contextBot)) - |> deliverOnMainQueue - |> beforeStarted { [weak self] in - if let strongSelf = self { - strongSelf.navigationContentNode?.setActivity(true) - } - } - |> afterCompleted { [weak self] in - if let strongSelf = self { - strongSelf.navigationContentNode?.setActivity(false) - } - } - } - - public var mediaPickerContext: WebSearchPickerContext? { - if let interaction = self.controllerInteraction { - return WebSearchPickerContext(interaction: interaction) - } else { - return nil - } - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.validLayout = layout - - let navigationBarHeight = self.navigationLayout(layout: layout).navigationFrame.maxY - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - } -} - -public class WebSearchPickerContext: AttachmentMediaPickerContext { - private weak var interaction: WebSearchControllerInteraction? - - public var selectionCount: Signal { - return Signal { [weak self] subscriber in - let disposable = self?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in - subscriber.putNext(Int(self?.interaction?.selectionState?.count() ?? 0)) - }, error: { _ in }, completed: { }) - return ActionDisposable { - disposable?.dispose() - } - } - } - - public var caption: Signal { - return Signal { [weak self] subscriber in - let disposable = self?.interaction?.editingState.forcedCaption().start(next: { caption in - if let caption = caption as? NSAttributedString { - subscriber.putNext(caption) - } else { - subscriber.putNext(nil) - } - }, error: { _ in }, completed: { }) - return ActionDisposable { - disposable?.dispose() - } - } - } - - init(interaction: WebSearchControllerInteraction) { - self.interaction = interaction - } - - public func setCaption(_ caption: NSAttributedString) { - self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true) - } - - public func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { - self.interaction?.sendSelected(nil, mode == .silently, nil, parameters) - } - - public func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - self.interaction?.schedule(parameters) - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift deleted file mode 100644 index 0aabc3e62a..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift +++ /dev/null @@ -1,829 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import SwiftSignalKit -import Display -import TelegramCore -import LegacyComponents -import TelegramPresentationData -import TelegramUIPreferences -import MergeLists -import AccountContext -import GalleryUI -import ChatListSearchItemHeader -import SegmentedControlNode -import AppBundle - -private struct WebSearchContextResultStableId: Hashable { - let result: ChatContextResult - - func hash(into hasher: inout Hasher) { - hasher.combine(result.id) - } - - static func ==(lhs: WebSearchContextResultStableId, rhs: WebSearchContextResultStableId) -> Bool { - return lhs.result == rhs.result - } -} - -private struct WebSearchEntry: Comparable, Identifiable { - let index: Int - let result: ChatContextResult - - var stableId: WebSearchContextResultStableId { - return WebSearchContextResultStableId(result: self.result) - } - - static func ==(lhs: WebSearchEntry, rhs: WebSearchEntry) -> Bool { - return lhs.index == rhs.index && lhs.result == rhs.result - } - - static func <(lhs: WebSearchEntry, rhs: WebSearchEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, controllerInteraction: WebSearchControllerInteraction) -> GridItem { - return WebSearchItem(account: account, theme: theme, interfaceState: interfaceState, result: self.result, controllerInteraction: controllerInteraction) - } -} - -private struct WebSearchTransition { - let deleteItems: [Int] - let insertItems: [GridNodeInsertItem] - let updateItems: [GridNodeUpdateItem] - let entryCount: Int - let hasMore: Bool -} - -private func preparedTransition(from fromEntries: [WebSearchEntry], to toEntries: [WebSearchEntry], hasMore: Bool, account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, controllerInteraction: WebSearchControllerInteraction) -> WebSearchTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, interfaceState: interfaceState, controllerInteraction: controllerInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, interfaceState: interfaceState, controllerInteraction: controllerInteraction)) } - - return WebSearchTransition(deleteItems: deleteIndices, insertItems: insertions, updateItems: updates, entryCount: toEntries.count, hasMore: hasMore) -} - -private func gridNodeLayoutForContainerLayout(_ layout: ContainerViewLayout) -> GridNodeLayoutType { - let itemsPerRow: Int - if case .compact = layout.metrics.widthClass { - switch layout.orientation { - case .portrait: - itemsPerRow = 3 - case .landscape: - itemsPerRow = 5 - } - } else { - itemsPerRow = 3 - } - - let side = floorToScreenPixels((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)) - return .fixed(itemSize: CGSize(width: side, height: side), fillWidth: true, lineSpacing: 1.0, itemSpacing: 1.0) -} - - -private struct WebSearchRecentQueryStableId: Hashable { - let query: String -} - -private struct WebSearchRecentQueryEntry: Comparable, Identifiable { - let index: Int - let query: String - - var stableId: WebSearchRecentQueryStableId { - return WebSearchRecentQueryStableId(query: self.query) - } - - static func ==(lhs: WebSearchRecentQueryEntry, rhs: WebSearchRecentQueryEntry) -> Bool { - return lhs.index == rhs.index && lhs.query == rhs.query - } - - static func <(lhs: WebSearchRecentQueryEntry, rhs: WebSearchRecentQueryEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction, header: ListViewItemHeader) -> ListViewItem { - return WebSearchRecentQueryItem(account: account, theme: theme, strings: strings, query: self.query, tapped: { query in - controllerInteraction.setSearchQuery(query) - }, deleted: { query in - controllerInteraction.deleteRecentQuery(query) - }, header: header) - } -} - -private struct WebSearchRecentTransition { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] -} - -private func preparedWebSearchRecentTransition(from fromEntries: [WebSearchRecentQueryEntry], to toEntries: [WebSearchRecentQueryEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction, header: ListViewItemHeader) -> WebSearchRecentTransition { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, controllerInteraction: controllerInteraction, header: header), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, controllerInteraction: controllerInteraction, header: header), directionHint: nil) } - - return WebSearchRecentTransition(deletions: deletions, insertions: insertions, updates: updates) -} - -class WebSearchControllerNode: ASDisplayNode { - private weak var controller: WebSearchController? - private let context: AccountContext - private let peer: EnginePeer? - private let chatLocation: ChatLocation? - private var theme: PresentationTheme - private var strings: PresentationStrings - private var presentationData: PresentationData - private let mode: WebSearchMode - private let attachment: Bool - - private let controllerInteraction: WebSearchControllerInteraction - private var webSearchInterfaceState: WebSearchInterfaceState - private let webSearchInterfaceStatePromise: ValuePromise - - private let segmentedContainerNode: ASDisplayNode - private let segmentedBackgroundNode: ASDisplayNode - private let segmentedSeparatorNode: ASDisplayNode - private let segmentedControlNode: SegmentedControlNode - - private let toolbarBackgroundNode: ASDisplayNode - private let toolbarSeparatorNode: ASDisplayNode - private let cancelButton: HighlightableButtonNode - private let sendButton: HighlightableButtonNode - private let badgeNode: WebSearchBadgeNode - - private let attributionNode: ASImageNode - - private let recentQueriesNode: ListView - private var enqueuedRecentTransitions: [(WebSearchRecentTransition, Bool)] = [] - - private var gridNode: GridNode - private var enqueuedTransitions: [(WebSearchTransition, Bool)] = [] - private var dequeuedInitialTransitionOnLayout = false - - private(set) var currentExternalResults: ChatContextResultCollection? - private var currentProcessedResults: ChatContextResultCollection? - private var currentEntries: [WebSearchEntry]? - private var hasMore = false - private var isLoadingMore = false - - private let hiddenMediaId = Promise(nil) - private var hiddenMediaDisposable: Disposable? - - private let results = ValuePromise(nil, ignoreRepeated: true) - - private let disposable = MetaDisposable() - private let loadMoreDisposable = MetaDisposable() - - private var recentDisposable: Disposable? - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - var requestUpdateInterfaceState: (Bool, (WebSearchInterfaceState) -> WebSearchInterfaceState) -> Void = { _, _ in } - var cancel: (() -> Void)? - var dismissInput: (() -> Void)? - - var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } - - init(controller: WebSearchController, context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: EnginePeer?, chatLocation: ChatLocation?, mode: WebSearchMode, attachment: Bool) { - self.controller = controller - self.context = context - self.theme = presentationData.theme - self.strings = presentationData.strings - self.presentationData = presentationData - self.controllerInteraction = controllerInteraction - self.peer = peer - self.chatLocation = chatLocation - self.mode = mode - self.attachment = attachment - - self.webSearchInterfaceState = WebSearchInterfaceState(presentationData: context.sharedContext.currentPresentationData.with { $0 }) - self.webSearchInterfaceStatePromise = ValuePromise(self.webSearchInterfaceState, ignoreRepeated: true) - - self.segmentedContainerNode = ASDisplayNode() - self.segmentedContainerNode.clipsToBounds = true - - self.segmentedBackgroundNode = ASDisplayNode() - self.segmentedSeparatorNode = ASDisplayNode() - - let items = [ - strings.WebSearch_Images, - strings.WebSearch_GIFs - ] - self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: items.map { SegmentedControlItem(title: $0) }, selectedIndex: 0) - - self.toolbarBackgroundNode = ASDisplayNode() - self.toolbarSeparatorNode = ASDisplayNode() - - self.attributionNode = ASImageNode() - - self.cancelButton = HighlightableButtonNode() - self.sendButton = HighlightableButtonNode() - - self.badgeNode = WebSearchBadgeNode(theme: theme) - - self.gridNode = GridNode() - self.gridNode.backgroundColor = theme.list.plainBackgroundColor - - self.recentQueriesNode = ListViewImpl() - self.recentQueriesNode.backgroundColor = theme.list.plainBackgroundColor - self.recentQueriesNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - super.init() - - self.setViewBlock({ - return UITracingLayerView() - }) - - self.addSubnode(self.gridNode) - if !attachment { - self.addSubnode(self.recentQueriesNode) - } - self.addSubnode(self.segmentedContainerNode) - self.segmentedContainerNode.addSubnode(self.segmentedBackgroundNode) - self.segmentedContainerNode.addSubnode(self.segmentedSeparatorNode) -// if case .media = mode { -// self.segmentedContainerNode.addSubnode(self.segmentedControlNode) -// } - if !attachment { - self.addSubnode(self.toolbarBackgroundNode) - self.addSubnode(self.toolbarSeparatorNode) - self.addSubnode(self.cancelButton) - self.addSubnode(self.sendButton) - self.addSubnode(self.attributionNode) - self.addSubnode(self.badgeNode) - } - - self.segmentedControlNode.selectedIndexChanged = { [weak self] index in - if let strongSelf = self, let scope = WebSearchScope(rawValue: Int32(index)) { - let _ = updateWebSearchSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager) { _ -> WebSearchSettings in - return WebSearchSettings(scope: scope) - }.start() - strongSelf.requestUpdateInterfaceState(true) { current in - return current.withUpdatedScope(scope) - } - } - } - self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: .touchUpInside) - - self.applyPresentationData() - - self.disposable.set((combineLatest(self.results.get(), self.webSearchInterfaceStatePromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] results, interfaceState in - if let strongSelf = self { - strongSelf.updateInternalResults(results, interfaceState: interfaceState) - } - })) - - if !attachment { - let previousRecentItems = Atomic<[WebSearchRecentQueryEntry]?>(value: nil) - self.recentDisposable = (combineLatest(webSearchRecentQueries(engine: self.context.engine), self.webSearchInterfaceStatePromise.get()) - |> deliverOnMainQueue).start(next: { [weak self] queries, interfaceState in - if let strongSelf = self { - var entries: [WebSearchRecentQueryEntry] = [] - for i in 0 ..< queries.count { - entries.append(WebSearchRecentQueryEntry(index: i, query: queries[i])) - } - - let header = ChatListSearchItemHeader(type: .recentPeers, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, actionTitle: interfaceState.presentationData.strings.WebSearch_RecentSectionClear, action: { _ in - let _ = clearRecentWebSearchQueries(engine: strongSelf.context.engine).start() - }) - - let previousEntries = previousRecentItems.swap(entries) - - let transition = preparedWebSearchRecentTransition(from: previousEntries ?? [], to: entries, account: strongSelf.context.account, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction, header: header) - strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil) - } - }) - } - - self.recentQueriesNode.beganInteractiveDragging = { [weak self] _ in - self?.dismissInput?() - } - - self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in - if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries { - if bottom.0 >= entries.count - 8 { - strongSelf.loadMore() - } - } - } - self.gridNode.scrollingInitiated = { [weak self] in - self?.dismissInput?() - } - - self.sendButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self, strongSelf.badgeNode.alpha > 0.0 { - if highlighted { - strongSelf.badgeNode.layer.removeAnimation(forKey: "opacity") - strongSelf.badgeNode.alpha = 0.4 - } else { - strongSelf.badgeNode.alpha = 1.0 - strongSelf.badgeNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - - self.hiddenMediaDisposable = (self.hiddenMediaId.get() - |> deliverOnMainQueue).start(next: { [weak self] id in - if let strongSelf = self { - strongSelf.controllerInteraction.hiddenMediaId = id - - strongSelf.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? WebSearchItemNode { - itemNode.updateHiddenMedia() - } - } - } - }) - } - - deinit { - self.disposable.dispose() - self.recentDisposable?.dispose() - self.loadMoreDisposable.dispose() - self.hiddenMediaDisposable?.dispose() - } - - func updatePresentationData(theme: PresentationTheme, strings: PresentationStrings) { - let themeUpdated = theme !== self.theme - self.theme = theme - self.strings = strings - - self.applyPresentationData(themeUpdated: themeUpdated) - } - - func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { - self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: transition) - transition.updateAlpha(node: self.segmentedBackgroundNode, alpha: alpha) - } - - func applyPresentationData(themeUpdated: Bool = true) { - self.cancelButton.setTitle(self.strings.Common_Cancel, with: Font.regular(17.0), with: self.theme.rootController.navigationBar.accentTextColor, for: .normal) - - if let selectionState = self.controllerInteraction.selectionState { - let sendEnabled = selectionState.count() > 0 - let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor - self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal) - } - - if themeUpdated { - self.gridNode.backgroundColor = self.theme.list.plainBackgroundColor - self.segmentedBackgroundNode.backgroundColor = self.theme.list.plainBackgroundColor - self.segmentedSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor - self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme)) - self.toolbarBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.opaqueBackgroundColor - self.toolbarSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor - } - - let gifProviderImage: UIImage? - if let gifProvider = self.webSearchInterfaceState.gifProvider { - switch gifProvider { - case "tenor": - gifProviderImage = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Tenor"), color: self.theme.list.itemSecondaryTextColor) - case "giphy": - gifProviderImage = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Giphy"), color: self.theme.list.itemSecondaryTextColor) - default: - gifProviderImage = nil - } - } else { - gifProviderImage = nil - } - let previousGifProviderImage = self.attributionNode.image - self.attributionNode.image = gifProviderImage - - if previousGifProviderImage == nil, let validLayout = self.containerLayout { - self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate) - } - } - - func animateIn(completion: (() -> Void)? = nil) { - self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - } - - func animateOut(completion: (() -> Void)? = nil) { - self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in - completion?() - }) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.input]) - insets.top += navigationBarHeight - - let hasQuery = !(self.webSearchInterfaceState.state?.query ?? "").isEmpty - - let segmentedHeight: CGFloat = self.segmentedControlNode.supernode != nil ? 44.0 : 5.0 - let panelY: CGFloat = insets.top - UIScreenPixel - 4.0 - - transition.updateSublayerTransformOffset(layer: self.segmentedContainerNode.layer, offset: CGPoint(x: 0.0, y: !hasQuery ? -44.0 : 0.0), completion: nil) - transition.updateFrame(node: self.segmentedContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: segmentedHeight))) - - transition.updateFrame(node: self.segmentedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: segmentedHeight))) - transition.updateFrame(node: self.segmentedSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: segmentedHeight - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - - let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 10.0 * 2.0), transition: transition) - transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - controlSize.width) / 2.0), y: 5.0), size: controlSize)) - - insets.top -= 4.0 - - let toolbarHeight: CGFloat = 44.0 - let toolbarY = layout.size.height - toolbarHeight - insets.bottom - transition.updateFrame(node: self.toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarY), size: CGSize(width: layout.size.width, height: toolbarHeight + insets.bottom))) - transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarY), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - - if let image = self.attributionNode.image { - self.attributionNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - image.size.width) / 2.0), y: toolbarY + floor((toolbarHeight - image.size.height) / 2.0)), size: image.size) - transition.updateAlpha(node: self.attributionNode, alpha: self.webSearchInterfaceState.state?.scope == .gifs ? 1.0 : 0.0) - } - - let toolbarPadding: CGFloat = 10.0 - let cancelSize = self.cancelButton.measure(CGSize(width: layout.size.width, height: toolbarHeight)) - transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: toolbarPadding + layout.safeInsets.left, y: toolbarY), size: CGSize(width: cancelSize.width, height: toolbarHeight))) - - let sendSize = self.sendButton.measure(CGSize(width: layout.size.width, height: toolbarHeight)) - let sendFrame = CGRect(origin: CGPoint(x: layout.size.width - toolbarPadding - layout.safeInsets.right - sendSize.width, y: toolbarY), size: CGSize(width: sendSize.width, height: toolbarHeight)) - transition.updateFrame(node: self.sendButton, frame: sendFrame) - - if let selectionState = self.controllerInteraction.selectionState { - self.sendButton.isHidden = false - - let previousSendEnabled = self.sendButton.isEnabled - let sendEnabled = selectionState.count() > 0 - self.sendButton.isEnabled = sendEnabled - if sendEnabled != previousSendEnabled { - let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor - self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal) - } - - let selectedCount = selectionState.count() - let badgeText = String(selectedCount) - if selectedCount > 0 && (self.badgeNode.text != badgeText || self.badgeNode.alpha < 1.0) { - if transition.isAnimated { - var incremented = true - if let previousCount = Int(self.badgeNode.text) { - incremented = selectedCount > previousCount || self.badgeNode.alpha < 1.0 - } - self.badgeNode.animateBump(incremented: incremented) - } - self.badgeNode.text = badgeText - - let badgeSize = self.badgeNode.measure(layout.size) - transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize)) - transition.updateAlpha(node: self.badgeNode, alpha: 1.0) - } else if selectedCount == 0 { - if transition.isAnimated { - self.badgeNode.animateOut() - } - let badgeSize = CGSize(width: 22.0, height: 22.0) - transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize)) - transition.updateAlpha(node: self.badgeNode, alpha: 0.0) - } - } else { - self.sendButton.isHidden = true - } - - let previousBounds = self.gridNode.bounds - self.gridNode.bounds = CGRect(x: previousBounds.origin.x, y: previousBounds.origin.y, width: layout.size.width, height: layout.size.height) - self.gridNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - - insets.top += segmentedHeight - insets.bottom += toolbarHeight - - let gridInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: insets.bottom, right: layout.safeInsets.right) - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: gridInsets, preloadSize: 400.0, type: gridNodeLayoutForContainerLayout(layout)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in }) - - let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - - self.recentQueriesNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.recentQueriesNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - - if !self.dequeuedInitialTransitionOnLayout { - self.dequeuedInitialTransitionOnLayout = true - self.dequeueTransition() - } - } - - func updateInterfaceState(_ interfaceState: WebSearchInterfaceState, animated: Bool) { - let previousGifProvider = self.webSearchInterfaceState.gifProvider - self.webSearchInterfaceState = interfaceState - self.webSearchInterfaceStatePromise.set(self.webSearchInterfaceState) - - if let state = interfaceState.state { - self.segmentedControlNode.selectedIndex = Int(state.scope.rawValue) - } - - if previousGifProvider != interfaceState.gifProvider { - self.applyPresentationData(themeUpdated: false) - } - - if let validLayout = self.containerLayout { - self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) - } - } - - func updateSelectionState(animated: Bool) { - self.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? WebSearchItemNode { - itemNode.updateSelectionState(animated: animated) - } - } - - if let validLayout = self.containerLayout { - self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) - } - } - - func updateResults(_ results: ChatContextResultCollection?, immediate: Bool = false) { - if self.currentExternalResults == results { - return - } - let previousResults = self.currentExternalResults - self.currentExternalResults = results - self.currentProcessedResults = results - - self.isLoadingMore = false - self.loadMoreDisposable.set(nil) - - if immediate && previousResults?.query == results?.query && previousResults?.botId != results?.botId { - let previousNode = self.gridNode - - let gridNode = GridNode() - gridNode.backgroundColor = theme.list.plainBackgroundColor - gridNode.frame = previousNode.frame - - gridNode.visibleItemsUpdated = { [weak self] visibleItems in - if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries { - if bottom.0 >= entries.count - 8 { - strongSelf.loadMore() - } - } - } - gridNode.scrollingInitiated = { [weak self] in - self?.dismissInput?() - } - - if self.recentQueriesNode.supernode != nil { - self.insertSubnode(gridNode, belowSubnode: self.recentQueriesNode) - } else { - self.insertSubnode(gridNode, aboveSubnode: previousNode) - } - self.gridNode = gridNode - self.currentEntries = nil - let directionMultiplier: CGFloat - if let state = self.webSearchInterfaceState.state { - switch state.scope { - case .images: - directionMultiplier = 1.0 - case .gifs: - directionMultiplier = -1.0 - } - } else { - directionMultiplier = 1.0 - } - - previousNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -directionMultiplier * self.bounds.width, y: 0.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak previousNode] _ in - previousNode?.removeFromSupernode() - }) - gridNode.layer.animatePosition(from: CGPoint(x: directionMultiplier * bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } else if previousResults?.botId != results?.botId || previousResults?.query != results?.query { - self.scrollToTop() - } - - self.results.set(results) - } - - func clearResults() { - self.results.set(nil) - } - - private func loadMore() { - guard !self.isLoadingMore, let currentProcessedResults = self.currentProcessedResults, currentProcessedResults.results.count > 55, let nextOffset = currentProcessedResults.nextOffset else { - return - } - self.isLoadingMore = true - let geoPoint = currentProcessedResults.geoPoint.flatMap { geoPoint -> (Double, Double) in - return (geoPoint.latitude, geoPoint.longitude) - } - self.loadMoreDisposable.set((self.context.engine.messages.requestChatContextResults(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(geoPoint), offset: nextOffset) - |> deliverOnMainQueue).start(next: { [weak self] nextResults in - guard let strongSelf = self, let nextResults = nextResults else { - return - } - strongSelf.isLoadingMore = false - var results: [ChatContextResult] = [] - var existingIds = Set() - for result in currentProcessedResults.results { - results.append(result) - existingIds.insert(result.id) - } - for result in nextResults.results.results { - if !existingIds.contains(result.id) { - results.append(result) - existingIds.insert(result.id) - } - } - let mergedResults = ChatContextResultCollection(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, geoPoint: currentProcessedResults.geoPoint, queryId: nextResults.results.queryId, nextOffset: nextResults.results.nextOffset, presentation: currentProcessedResults.presentation, switchPeer: currentProcessedResults.switchPeer, webView: currentProcessedResults.webView, results: results, cacheTimeout: currentProcessedResults.cacheTimeout) - strongSelf.currentProcessedResults = mergedResults - strongSelf.results.set(mergedResults) - })) - } - - private func updateInternalResults(_ results: ChatContextResultCollection?, interfaceState: WebSearchInterfaceState) { - var entries: [WebSearchEntry] = [] - var hasMore = false - if let state = interfaceState.state, state.query.isEmpty { - } else if let results = results { - hasMore = results.nextOffset != nil - - var index = 0 - var resultIds = Set() - for result in results.results { - let entry = WebSearchEntry(index: index, result: result) - if resultIds.contains(entry.stableId) { - continue - } else { - resultIds.insert(entry.stableId) - } - entries.append(entry) - index += 1 - } - } - - let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hasMore: hasMore, account: self.context.account, theme: interfaceState.presentationData.theme, interfaceState: interfaceState, controllerInteraction: self.controllerInteraction) - self.currentEntries = entries - - self.enqueueTransition(transition, firstTime: firstTime) - } - - private func enqueueTransition(_ transition: WebSearchTransition, firstTime: Bool) { - self.enqueuedTransitions.append((transition, firstTime)) - - if self.containerLayout != nil { - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } - } - } - - private func dequeueTransition() { - if let (transition, _) = self.enqueuedTransitions.first { - self.enqueuedTransitions.remove(at: 0) - - if let state = self.webSearchInterfaceState.state { - self.recentQueriesNode.isHidden = !state.query.isEmpty - } - - self.hasMore = transition.hasMore - self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, synchronousLoads: true), completion: { _ in }) - } - } - - private func enqueueRecentTransition(_ transition: WebSearchRecentTransition, firstTime: Bool) { - enqueuedRecentTransitions.append((transition, firstTime)) - - if self.containerLayout != nil { - while !self.enqueuedRecentTransitions.isEmpty { - self.dequeueRecentTransition() - } - } - } - - private func dequeueRecentTransition() { - if let (transition, firstTime) = self.enqueuedRecentTransitions.first { - self.enqueuedRecentTransitions.remove(at: 0) - - var options = ListViewDeleteAndInsertOptions() - if firstTime { - options.insert(.PreferSynchronousDrawing) - } else { - options.insert(.AnimateInsertion) - } - - self.recentQueriesNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in - }) - } - } - - @objc private func cancelPressed() { - self.cancel?() - } - - @objc private func sendPressed() { - self.controllerInteraction.sendSelected(nil, false, nil, nil) - - self.cancel?() - } - - func scrollToTop(animated: Bool = false) { - self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.gridNode.scrollView.contentInset.top), animated: animated) - } - - func openResult(currentResult: ChatContextResult, present: @escaping (ViewController, Any?) -> Void) { - self.view.endEditing(true) - - if self.controllerInteraction.selectionState != nil { - if let state = self.webSearchInterfaceState.state, state.scope == .images { - if let results = self.currentProcessedResults?.results { - presentLegacyWebSearchGallery(context: self.context, peer: self.peer, threadTitle: nil, chatLocation: self.chatLocation, presentationData: self.presentationData, results: results, current: currentResult, selectionContext: self.controllerInteraction.selectionState, editingContext: self.controllerInteraction.editingState, updateHiddenMedia: { [weak self] id in - self?.hiddenMediaId.set(.single(id)) - }, initialLayout: self.containerLayout?.0, transitionHostView: { [weak self] in - return self?.gridNode.view - }, transitionView: { [weak self] result in - return self?.transitionNode(for: result)?.transitionView() - }, completed: { [weak self] result in - if let strongSelf = self { - strongSelf.controllerInteraction.sendSelected(result, false, nil, nil) - strongSelf.cancel?() - } - }, getCaptionPanelView: self.getCaptionPanelView, present: present) - } - } else { - if let results = self.currentProcessedResults?.results { - var entries: [WebSearchGalleryEntry] = [] - var centralIndex: Int = 0 - for i in 0 ..< results.count { - entries.append(WebSearchGalleryEntry(index: entries.count, result: results[i])) - if results[i] == currentResult { - centralIndex = i - } - } - - let controller = WebSearchGalleryController(context: self.context, peer: self.peer, selectionState: self.controllerInteraction.selectionState, editingState: self.controllerInteraction.editingState, entries: entries, centralIndex: centralIndex, replaceRootController: { (controller, _) in - - }, baseNavigationController: nil, sendCurrent: { [weak self] result in - if let strongSelf = self { - strongSelf.controllerInteraction.sendSelected(result, false, nil, nil) - strongSelf.cancel?() - } - }) - self.hiddenMediaId.set((controller.hiddenMedia |> deliverOnMainQueue) - |> map { entry in - return entry?.result.id - }) - present(controller, WebSearchGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in - if let strongSelf = self { - var transitionNode: WebSearchItemNode? - strongSelf.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == entry.result.id { - transitionNode = itemNode - } - } - if let transitionNode = transitionNode { - return GalleryTransitionArguments(transitionNode: (transitionNode, transitionNode.bounds, { [weak transitionNode] in - return (transitionNode?.transitionView().snapshotContentTree(unhide: true), nil) - }), addToTransitionSurface: { view in - if let strongSelf = self { - strongSelf.gridNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.gridNode.view) - } - }) - } - } - return nil - })) - } - } - } else { - if let mode = self.controller?.mode, case let .editor(completion) = mode { - if let item = legacyWebSearchItem(engine: self.context.engine, result: currentResult) { - let _ = (item.originalImage - |> deliverOnMainQueue).start(next: { image in - if !image.degraded() { - completion(image) - } - }) - } - } else { - presentLegacyWebSearchEditor(context: self.context, theme: self.theme, result: currentResult, initialLayout: self.containerLayout?.0, updateHiddenMedia: { [weak self] id in - self?.hiddenMediaId.set(.single(id)) - }, transitionHostView: { [weak self] in - return self?.gridNode.view - }, transitionView: { [weak self] result in - return self?.transitionNode(for: result)?.transitionView() - }, completed: { [weak self] result in - if let strongSelf = self { - strongSelf.controllerInteraction.avatarCompleted(result) - strongSelf.cancel?() - } - }, present: present) - } - } - } - - private func transitionNode(for result: ChatContextResult) -> WebSearchItemNode? { - var transitionNode: WebSearchItemNode? - self.gridNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == result.id { - transitionNode = itemNode - } - } - return transitionNode - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift deleted file mode 100644 index dcf02efb5f..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift +++ /dev/null @@ -1,354 +0,0 @@ -import Foundation -import UIKit -import Display -import QuickLook -import SwiftSignalKit -import AsyncDisplayKit -import TelegramCore -import LegacyComponents -import TelegramPresentationData -import AccountContext -import GalleryUI -import TelegramUniversalVideoContent - -final class WebSearchGalleryControllerInteraction { - let dismiss: (Bool) -> Void - let send: (ChatContextResult) -> Void - let selectionState: TGMediaSelectionContext? - let editingState: TGMediaEditingContext - - init(dismiss: @escaping (Bool) -> Void, send: @escaping (ChatContextResult) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { - self.dismiss = dismiss - self.send = send - self.selectionState = selectionState - self.editingState = editingState - } -} - -struct WebSearchGalleryEntry: Equatable { - let index: Int - let result: ChatContextResult - - static func ==(lhs: WebSearchGalleryEntry, rhs: WebSearchGalleryEntry) -> Bool { - return lhs.result == rhs.result - } - - func item(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchGalleryControllerInteraction?) -> GalleryItem { - switch self.result { - case let .externalReference(externalReference): - if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let dimensions = content.dimensions { - let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) - return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true, storeAfterDownload: nil), controllerInteraction: controllerInteraction) - } - case let .internalReference(internalReference): - if let file = internalReference.file { - return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: .standalone(media: file), loopVideo: true, enableSound: false, fetchAutomatically: true, storeAfterDownload: nil), controllerInteraction: controllerInteraction) - } - } - preconditionFailure() - } -} - -final class WebSearchGalleryControllerPresentationArguments { - let animated: Bool - let transitionArguments: (WebSearchGalleryEntry) -> GalleryTransitionArguments? - - init(animated: Bool = true, transitionArguments: @escaping (WebSearchGalleryEntry) -> GalleryTransitionArguments?) { - self.animated = animated - self.transitionArguments = transitionArguments - } -} - -class WebSearchGalleryController: ViewController { - private static let navigationTheme = NavigationBarTheme(overallDarkAppearance: false, buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear, accentButtonColor: .white, accentDisabledButtonColor: .white, accentForegroundColor: .black) - - private var galleryNode: GalleryControllerNode { - return self.displayNode as! GalleryControllerNode - } - - private let context: AccountContext - private var presentationData: PresentationData - private var controllerInteraction: WebSearchGalleryControllerInteraction? - - private let _ready = Promise() - override var ready: Promise { - return self._ready - } - private var didSetReady = false - - private let disposable = MetaDisposable() - - private var entries: [WebSearchGalleryEntry] = [] - private var centralEntryIndex: Int? - - private let centralItemTitle = Promise() - private let centralItemTitleContent = Promise() - private let centralItemNavigationStyle = Promise() - private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>() - private let centralItemAttributesDisposable = DisposableSet() - - private let titleView: GalleryTitleView - - private let checkedDisposable = MetaDisposable() - private var checkNode: GalleryNavigationCheckNode? - - private let _hiddenMedia = Promise(nil) - var hiddenMedia: Signal { - return self._hiddenMedia.get() - } - - private let replaceRootController: (ViewController, Promise?) -> Void - private let baseNavigationController: NavigationController? - - init(context: AccountContext, peer: EnginePeer?, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext, entries: [WebSearchGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, Promise?) -> Void, baseNavigationController: NavigationController?, sendCurrent: @escaping (ChatContextResult) -> Void) { - self.context = context - self.replaceRootController = replaceRootController - self.baseNavigationController = baseNavigationController - - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - - self.titleView = GalleryTitleView(context: context, presentationData: self.presentationData) - - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: WebSearchGalleryController.navigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) - - self.controllerInteraction = WebSearchGalleryControllerInteraction(dismiss: { [weak self] animated in - self?.dismiss(forceAway: false) - }, send: { [weak self] current in - sendCurrent(current) - self?.dismiss(forceAway: true) - }, selectionState: selectionState, editingState: editingState) - - if let title = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) { - let recipientNode = GalleryNavigationRecipientNode(color: .white, title: title) - let leftItem = UIBarButtonItem(customDisplayNode: recipientNode) - self.navigationItem.leftBarButtonItem = leftItem - } - - let checkNode = GalleryNavigationCheckNode(theme: self.presentationData.theme) - checkNode.addTarget(target: self, action: #selector(self.checkPressed)) - let rightItem = UIBarButtonItem(customDisplayNode: checkNode) - self.navigationItem.rightBarButtonItem = rightItem - self.checkNode = checkNode - - self.statusBar.statusBarStyle = .White - - let entriesSignal: Signal<[WebSearchGalleryEntry], NoError> = .single(entries) - - self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in - if let strongSelf = self { - strongSelf.entries = entries - strongSelf.centralEntryIndex = centralIndex - if strongSelf.isViewLoaded { - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ - $0.item(context: context, presentationData: strongSelf.presentationData, controllerInteraction: strongSelf.controllerInteraction) - }), centralItemIndex: centralIndex) - - let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in - strongSelf?.didSetReady = true - } - strongSelf._ready.set(ready |> map { true }) - } - } - })) - - self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in - self?.navigationItem.title = title - })) - - self.centralItemAttributesDisposable.add(self.centralItemTitleContent.get().start(next: { [weak self] titleContent in - self?.titleView.setContent(content: titleContent) - })) - - self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in - self?.galleryNode.updatePresentationState({ - $0.withUpdatedFooterContentNode(footerContentNode) - }, transition: .immediate) - })) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.disposable.dispose() - self.checkedDisposable.dispose() - self.centralItemAttributesDisposable.dispose() - } - - @objc func checkPressed() { - if let checkNode = self.checkNode, let controllerInteraction = self.controllerInteraction, let centralItemNode = self.galleryNode.pager.centralItemNode() as? WebSearchVideoGalleryItemNode, let item = centralItemNode.item { - let legacyItem = LegacyWebSearchItem(result: item.result) - - controllerInteraction.selectionState?.setItem(legacyItem, selected: checkNode.isChecked) - } - } - - private func dismiss(forceAway: Bool, animated: Bool = true) { - var animatedOutNode = true - var animatedOutInterface = false - - let completion = { [weak self] in - if animatedOutNode && animatedOutInterface { - self?._hiddenMedia.set(.single(nil)) - self?.presentingViewController?.dismiss(animated: false, completion: nil) - } - } - - if animated { - if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments { - if !self.entries.isEmpty { - if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { - animatedOutNode = false - centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: { - animatedOutNode = true - completion() - }) - } - } - } - - self.galleryNode.animateOut(animateContent: animatedOutNode, completion: { - animatedOutInterface = true - completion() - }) - } else { - animatedOutInterface = true - completion() - } - } - - override func loadDisplayNode() { - let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in - if let strongSelf = self { - strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true) - } - }, pushController: { _ in - }, dismissController: { [weak self] in - self?.dismiss(forceAway: true) - }, replaceRootController: { [weak self] controller, ready in - if let strongSelf = self { - strongSelf.replaceRootController(controller, ready) - } - }, editMedia: { _ in - }, controller: { [weak self] in - return self - }, currentItemNode: { [weak self] in - return self?.galleryNode.pager.centralItemNode() - }) - self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction, titleView: self.titleView) - self.displayNodeDidLoad() - - self.galleryNode.statusBar = self.statusBar - self.galleryNode.navigationBar = self.navigationBar - - self.galleryNode.transitionDataForCentralItem = { [weak self] in - if let strongSelf = self { - if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? WebSearchGalleryControllerPresentationArguments { - if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) { - return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface) - } - } - } - return nil - } - self.galleryNode.dismiss = { [weak self] in - self?._hiddenMedia.set(.single(nil)) - self?.presentingViewController?.dismiss(animated: false, completion: nil) - } - - self.galleryNode.pager.replaceItems(self.entries.map({ - $0.item(context: self.context, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction) - }), centralItemIndex: self.centralEntryIndex) - - self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in - if let strongSelf = self { - var item: WebSearchGalleryEntry? - if let index = index { - item = strongSelf.entries[index] - - if let node = strongSelf.galleryNode.pager.centralItemNode() { - strongSelf.centralItemTitle.set(node.title()) - strongSelf.centralItemTitleContent.set(node.titleContent()) - strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) - strongSelf.centralItemFooterContentNode.set(node.footerContent()) - } - - if let checkNode = strongSelf.checkNode, let controllerInteraction = strongSelf.controllerInteraction, let selectionState = controllerInteraction.selectionState, let item = item { - checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: false) - } - } - if strongSelf.didSetReady { - strongSelf._hiddenMedia.set(.single(item)) - } - } - } - - let selectionState = self.controllerInteraction?.selectionState - let selectionUpdated = Signal { subscriber in - if let selectionState = selectionState { - let disposable = selectionState.selectionChangedSignal()!.start(next: { _ in - subscriber.putNext(Void()) - }, error: { _ in }, completed: {})! - return ActionDisposable { - disposable.dispose() - } - } else { - subscriber.putCompletion() - return EmptyDisposable - } - } - self.checkedDisposable.set((selectionUpdated - |> deliverOnMainQueue).start(next: { [weak self] _ in - if let strongSelf = self, let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() { - let item = strongSelf.entries[centralItemNode.index] - if let checkNode = strongSelf.checkNode, let controllerInteraction = strongSelf.controllerInteraction, let selectionState = controllerInteraction.selectionState { - checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: true) - } - } - })) - - let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in - self?.didSetReady = true - } - self._ready.set(ready |> map { true }) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - var nodeAnimatesItself = false - - if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments { - self.centralItemTitle.set(centralItemNode.title()) - self.centralItemTitleContent.set(centralItemNode.titleContent()) - self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) - self.centralItemFooterContentNode.set(centralItemNode.footerContent()) - - let item = self.entries[centralItemNode.index] - if let transitionArguments = presentationArguments.transitionArguments(item) { - nodeAnimatesItself = true - centralItemNode.activateAsInitial() - - if presentationArguments.animated { - centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {}) - } - - if let checkNode = self.checkNode, let controllerInteraction = self.controllerInteraction, let selectionState = controllerInteraction.selectionState { - checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: false) - } - - self._hiddenMedia.set(.single(self.entries[centralItemNode.index])) - } - } - - self.galleryNode.animateIn(animateContent: !nodeAnimatesItself, useSimpleAnimation: false) - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryFooterContentNode.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryFooterContentNode.swift deleted file mode 100644 index 8b355c575e..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchGalleryFooterContentNode.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import SwiftSignalKit -import LegacyComponents -import TelegramPresentationData -import AccountContext -import GalleryUI - -final class WebSearchGalleryFooterContentNode: GalleryFooterContentNode { - private let context: AccountContext - private var theme: PresentationTheme - private var strings: PresentationStrings - - private let cancelButton: HighlightableButtonNode - private let sendButton: HighlightableButtonNode - - var cancel: (() -> Void)? - var send: (() -> Void)? - - init(context: AccountContext, presentationData: PresentationData) { - self.context = context - self.theme = presentationData.theme - self.strings = presentationData.strings - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setImage(TGComponentsImageNamed("PhotoPickerBackIcon"), for: [.normal]) - self.sendButton = HighlightableButtonNode() - self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.theme), for: [.normal]) - - super.init() - - self.addSubnode(self.cancelButton) - self.addSubnode(self.sendButton) - - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) - } - - func setCaption(_ caption: String) { - - } - - override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> LayoutInfo { - let width = size.width - let panelSize: CGFloat = 49.0 - var panelHeight: CGFloat = panelSize + bottomInset - panelHeight += contentInset - - self.cancelButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - panelSize), size: CGSize(width: panelSize, height: panelSize)) - self.sendButton.frame = CGRect(origin: CGPoint(x: width - panelSize - rightInset, y: panelHeight - bottomInset - panelSize), size: CGSize(width: panelSize, height: panelSize)) - - return LayoutInfo(height: panelHeight, needsShadow: false) - } - - override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) { - self.cancelButton.alpha = 1.0 - self.sendButton.alpha = 1.0 - } - - override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { - self.cancelButton.alpha = 0.0 - self.sendButton.alpha = 0.0 - completion() - } - - @objc func cancelButtonPressed() { - self.cancel?() - } - - @objc func sendButtonPressed() { - self.send?() - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchInterfaceState.swift b/submodules/WebSearchUI/Sources/WebSearchInterfaceState.swift deleted file mode 100644 index ddeb5e1684..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchInterfaceState.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import UIKit -import TelegramPresentationData -import TelegramUIPreferences - -struct WebSearchInterfaceInnerState: Equatable { - let scope: WebSearchScope - let query: String -} - -struct WebSearchInterfaceState: Equatable { - let state: WebSearchInterfaceInnerState? - let presentationData: PresentationData - let gifProvider: String? - - init (presentationData: PresentationData) { - self.state = nil - self.presentationData = presentationData - self.gifProvider = nil - } - - init(state: WebSearchInterfaceInnerState?, presentationData: PresentationData, gifProvider: String? = nil) { - self.state = state - self.presentationData = presentationData - self.gifProvider = gifProvider - } - - func withUpdatedScope(_ scope: WebSearchScope) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(scope: scope, query: self.state?.query ?? ""), presentationData: self.presentationData, gifProvider: self.gifProvider) - } - - func withUpdatedQuery(_ query: String) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(scope: self.state?.scope ?? .images, query: query), presentationData: self.presentationData, gifProvider: self.gifProvider) - } - - func withUpdatedPresentationData(_ presentationData: PresentationData) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: self.state, presentationData: presentationData, gifProvider: self.gifProvider) - } - - func withUpdatedGifProvider(_ gifProvider: String?) -> WebSearchInterfaceState { - return WebSearchInterfaceState(state: self.state, presentationData: self.presentationData, gifProvider: gifProvider) - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchItem.swift b/submodules/WebSearchUI/Sources/WebSearchItem.swift deleted file mode 100644 index cc3b7f9587..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchItem.swift +++ /dev/null @@ -1,316 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import SwiftSignalKit -import TelegramPresentationData -import CheckNode -import PhotoResources -import RadialStatusNode - -final class WebSearchItem: GridItem { - var section: GridSection? - - let account: Account - let theme: PresentationTheme - let interfaceState: WebSearchInterfaceState - let result: ChatContextResult - let controllerInteraction: WebSearchControllerInteraction - - public init(account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, result: ChatContextResult, controllerInteraction: WebSearchControllerInteraction) { - self.account = account - self.theme = theme - self.result = result - self.interfaceState = interfaceState - self.controllerInteraction = controllerInteraction - } - - func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { - let node = WebSearchItemNode() - node.setup(item: self, synchronousLoad: synchronousLoad) - return node - } - - func update(node: GridItemNode) { - guard let node = node as? WebSearchItemNode else { - assertionFailure() - return - } - node.setup(item: self, synchronousLoad: false) - } -} - -final class WebSearchItemNode: GridItemNode { - private let imageNodeBackground: ASDisplayNode - private let imageNode: TransformImageNode - private var checkNode: CheckNode? - private var statusNode: RadialStatusNode? - - private(set) var item: WebSearchItem? - private var currentDimensions: CGSize? - - private let fetchStatusDisposable = MetaDisposable() - private let fetchDisposable = MetaDisposable() - private var resourceStatus: EngineMediaResource.FetchStatus? - - override init() { - self.imageNodeBackground = ASDisplayNode() - self.imageNodeBackground.isLayerBacked = true - - self.imageNode = TransformImageNode() - self.imageNode.contentAnimations = [.subsequentUpdates] - self.imageNode.displaysAsynchronously = false - - super.init() - - self.addSubnode(self.imageNodeBackground) - self.addSubnode(self.imageNode) - } - - deinit { - self.fetchStatusDisposable.dispose() - self.fetchDisposable.dispose() - } - - override func didLoad() { - super.didLoad() - - let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - recognizer.tapActionAtPoint = { _ in - return .waitForSingleTap - } - self.imageNode.view.addGestureRecognizer(recognizer) - } - - func updateProgress(_ value: Float?, animated: Bool) { - if let value { - let statusNode: RadialStatusNode - if let current = self.statusNode { - statusNode = current - } else { - statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6)) - statusNode.isUserInteractionEnabled = false - self.addSubnode(statusNode) - self.statusNode = statusNode - } - let adjustedProgress = max(0.027, CGFloat(value)) - let state: RadialStatusNodeState = .progress(color: .white, lineWidth: nil, value: adjustedProgress, cancelEnabled: true, animateRotation: true) - statusNode.transitionToState(state) - } else if let statusNode = self.statusNode { - self.statusNode = nil - if animated { - statusNode.transitionToState(.none, animated: true, completion: { [weak statusNode] in - statusNode?.removeFromSupernode() - }) - } else { - statusNode.removeFromSupernode() - } - } - } - - func setup(item: WebSearchItem, synchronousLoad: Bool) { - if self.item !== item { - var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - - var thumbnailDimensions: CGSize? - var thumbnailResource: TelegramMediaResource? - var imageResource: TelegramMediaResource? - var imageDimensions: CGSize? - var immediateThumbnailData: Data? - - switch item.result { - case let .externalReference(externalReference): - if let content = externalReference.content, externalReference.type != "gif" { - imageResource = content.resource - } else if let thumbnail = externalReference.thumbnail { - imageResource = thumbnail.resource - } - imageDimensions = externalReference.content?.dimensions?.cgSize - case let .internalReference(internalReference): - if let image = internalReference.image { - immediateThumbnailData = image.immediateThumbnailData - if let largestRepresentation = largestImageRepresentation(image.representations) { - imageDimensions = largestRepresentation.dimensions.cgSize - } - imageResource = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 200, height: 100))?.resource - if let file = internalReference.file { - if let thumbnailRepresentation = smallestImageRepresentation(file.previewRepresentations) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } else { - if let thumbnailRepresentation = smallestImageRepresentation(image.representations) { - thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize - thumbnailResource = thumbnailRepresentation.resource - } - } - } else if let file = internalReference.file { - immediateThumbnailData = file.immediateThumbnailData - if let dimensions = file.dimensions { - imageDimensions = dimensions.cgSize - } else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) { - imageDimensions = largestRepresentation.dimensions.cgSize - } - imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource - } - } - - var representations: [TelegramMediaImageRepresentation] = [] - if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) - } - if let imageResource = imageResource, let imageDimensions = imageDimensions { - representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) - } - if !representations.isEmpty { - let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - updateImageSignal = mediaGridMessagePhoto(account: item.account, userLocation: .other, photoReference: .standalone(media: tmpImage)) - } else { - updateImageSignal = .complete() - } - - if let updateImageSignal = updateImageSignal { - let editingContext = item.controllerInteraction.editingState - let editableItem = LegacyWebSearchItem(result: item.result) - let editedImageSignal = Signal { subscriber in - if let signal = editingContext.thumbnailImageSignal(for: editableItem) { - let disposable = signal.start(next: { next in - if let image = next as? UIImage { - subscriber.putNext(image) - } else { - subscriber.putNext(nil) - } - }, error: { _ in - }, completed: nil)! - - return ActionDisposable { - disposable.dispose() - } - } else { - return EmptyDisposable - } - } - let editedSignal: Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> = editedImageSignal - |> map { image in - if let image = image { - return { arguments in - guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { - return nil - } - let drawingRect = arguments.drawingRect - let imageSize = image.size - let fittedSize = imageSize.aspectFilled(arguments.boundingSize).fitted(imageSize) - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - - context.withFlippedContext { c in - c.setBlendMode(.copy) - if let cgImage = image.cgImage { - drawImage(context: c, image: cgImage, orientation: .up, in: fittedRect) - } - } - return context - } - } else { - return nil - } - } - - let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> = editedSignal - |> mapToSignal { result in - if result != nil { - return .single(result!) - } else { - return updateImageSignal - } - } - self.imageNode.setSignal(imageSignal) - } - - self.currentDimensions = imageDimensions - if let _ = imageDimensions { - self.setNeedsLayout() - } - self.updateHiddenMedia() - } - - self.item = item - self.updateSelectionState(animated: false) - } - - func updateSelectionState(animated: Bool) { - if self.checkNode == nil, let item = self.item, let _ = item.controllerInteraction.selectionState { - let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: item.theme, style: .overlay)) - checkNode.valueChanged = { [weak self] value in - guard let self else { - return - } - if !item.controllerInteraction.toggleSelection(item.result, value) { - self.checkNode?.setSelected(false, animated: false) - } - } - self.addSubnode(checkNode) - self.checkNode = checkNode - self.setNeedsLayout() - } - - if let item = self.item { - if let selectionState = item.controllerInteraction.selectionState { - let selected = selectionState.isIdentifierSelected(item.result.id) - self.checkNode?.setSelected(selected, animated: animated) - } - } - } - - func updateHiddenMedia() { - if let item = self.item { - let wasHidden = self.isHidden - self.isHidden = item.controllerInteraction.hiddenMediaId == item.result.id - if !self.isHidden && wasHidden { - self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - } - - func transitionView() -> UIView { - let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)! - view.frame = self.convert(self.bounds, to: nil) - return view - } - - override func layout() { - super.layout() - - let imageFrame = self.bounds - self.imageNode.frame = imageFrame - - if let item = self.item, let dimensions = self.currentDimensions { - let imageSize = dimensions.aspectFilled(imageFrame.size) - self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor))() - } - - let checkSize = CGSize(width: 28.0, height: 28.0) - self.checkNode?.frame = CGRect(origin: CGPoint(x: imageFrame.width - checkSize.width - 2.0, y: 2.0), size: checkSize) - } - - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - guard let item = self.item else { - return - } - - switch recognizer.state { - case .ended: - if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - item.controllerInteraction.openResult(item.result) - default: - break - } - } - default: - break - } - } -} - diff --git a/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift b/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift deleted file mode 100644 index f7af067849..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchNavigationContentNode.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import TelegramCore -import TelegramPresentationData -import SearchBarNode - -private let searchBarFont = Font.regular(17.0) - -final class WebSearchNavigationContentNode: NavigationBarContentNode { - private let theme: PresentationTheme - private let strings: PresentationStrings - - private let searchBar: SearchBarNode - - private var queryUpdated: ((String) -> Void)? - - var cancel: (() -> Void)? - - init(theme: PresentationTheme, strings: PresentationStrings, attachment: Bool) { - self.theme = theme - self.strings = strings - - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), presentationTheme: theme, strings: strings, fieldStyle: .modern, displayBackground: !attachment) - self.searchBar.hasCancelButton = attachment - self.searchBar.placeholderString = NSAttributedString(string: attachment ? strings.Attachment_SearchWeb : strings.Common_Search, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) - - super.init() - - self.addSubnode(self.searchBar) - - self.searchBar.textUpdated = { [weak self] query, _ in - self?.queryUpdated?(query) - } - self.searchBar.cancel = { [weak self] in - self?.cancel?() - } - } - - func setQueryUpdated(_ f: @escaping (String) -> Void) { - self.queryUpdated = f - } - - func setActivity(_ activity: Bool) { - self.searchBar.activity = activity - } - - func setQuery(_ query: String) { - self.searchBar.text = query - } - - override var nominalHeight: CGFloat { - return 56.0 - } - - override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { - let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 56.0)) - self.searchBar.frame = searchBarFrame - self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) - - return size - } - - func activate(select: Bool = false) { - self.searchBar.activate() - self.searchBar.selectAll() - } - - func deactivate() { - self.searchBar.deactivate(clear: false) - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchRecentQueries.swift b/submodules/WebSearchUI/Sources/WebSearchRecentQueries.swift deleted file mode 100644 index 01ba32939b..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchRecentQueries.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import UIKit -import TelegramCore -import SwiftSignalKit -import TelegramUIPreferences - -private struct WebSearchRecentQueryItemId { - public let rawValue: EngineMemoryBuffer - - var value: String { - return String(data: self.rawValue.makeData(), encoding: .utf8) ?? "" - } - - init(_ rawValue: EngineMemoryBuffer) { - self.rawValue = rawValue - } - - init?(_ value: String) { - if let data = value.data(using: .utf8) { - self.rawValue = EngineMemoryBuffer(data: data) - } else { - return nil - } - } -} - -public final class RecentWebSearchQueryItem: Codable { - init() { - } - - public init(from decoder: Decoder) throws { - } - - public func encode(to encoder: Encoder) throws { - } -} - -func addRecentWebSearchQuery(engine: TelegramEngine, string: String) -> Signal { - if let itemId = WebSearchRecentQueryItemId(string) { - return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries, id: itemId.rawValue, item: RecentWebSearchQueryItem(), removeTailIfCountExceeds: 100) - } else { - return .complete() - } -} - -func removeRecentWebSearchQuery(engine: TelegramEngine, string: String) -> Signal { - if let itemId = WebSearchRecentQueryItemId(string) { - return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries, id: itemId.rawValue) - } else { - return .complete() - } -} - -func clearRecentWebSearchQueries(engine: TelegramEngine) -> Signal { - return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries) -} - -func webSearchRecentQueries(engine: TelegramEngine) -> Signal<[String], NoError> { - return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries)) - |> map { items -> [String] in - var result: [String] = [] - for item in items { - let value = WebSearchRecentQueryItemId(item.id).value - result.append(value) - } - return result - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift b/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift deleted file mode 100644 index a6f0d79fac..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchRecentQueryItem.swift +++ /dev/null @@ -1,233 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display -import SwiftSignalKit -import TelegramCore -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils - -private enum RevealOptionKey: Int32 { - case delete -} - -public class WebSearchRecentQueryItem: ListViewItem { - let theme: PresentationTheme - let strings: PresentationStrings - let account: Account - let query: String - let tapped: (String) -> Void - let deleted: (String) -> Void - - let header: ListViewItemHeader? - - public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, query: String, tapped: @escaping (String) -> Void, deleted: @escaping (String) -> Void, header: ListViewItemHeader) { - self.theme = theme - self.strings = strings - self.account = account - self.query = query - self.tapped = tapped - self.deleted = deleted - self.header = header - } - - public 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 = WebSearchRecentQueryItemNode() - let makeLayout = node.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem == nil, !(previousItem is WebSearchRecentQueryItem)) - node.contentSize = nodeLayout.contentSize - node.insets = nodeLayout.insets - - completion(node, nodeApply) - } - } - - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? WebSearchRecentQueryItemNode { - let layout = nodeValue.asyncLayout() - async { - let (nodeLayout, apply) = layout(self, params, nextItem == nil, !(previousItem is WebSearchRecentQueryItem)) - Queue.mainQueue().async { - completion(nodeLayout, { info in - apply().1(info) - }) - } - } - } - } - } - - public var selectable: Bool { - return true - } - - public func selected(listView: ListView) { - listView.clearHighlightAnimated(true) - self.tapped(self.query) - } -} - -class WebSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { - private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - private var textNode: TextNode? - - private var item: WebSearchRecentQueryItem? - private var layoutParams: ListViewItemLayoutParams? - - required init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isLayerBacked = true - - super.init(layerBacked: false, rotated: false, seeThrough: false) - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - } - - override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - if let item = self.item { - let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil, previousItem == nil) - self.contentSize = nodeLayout.contentSize - self.insets = nodeLayout.insets - let _ = nodeApply() - } - } - - override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { - super.setHighlighted(highlighted, at: point, animated: animated) - - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } - } - - func asyncLayout() -> (_ item: WebSearchRecentQueryItem, _ params: ListViewItemLayoutParams, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { - let currentItem = self.item - - let textLayout = TextNode.asyncLayout(self.textNode) - - return { [weak self] item, params, last, firstWithHeader in - - let leftInset: CGFloat = 15.0 + params.leftInset - let rightInset: CGFloat = params.rightInset - - let attributedString = NSAttributedString(string: item.query, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) - let textApply = textLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - - let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) - - return (nodeLayout, { [weak self] in - var updatedTheme: PresentationTheme? - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - } - - return (nil, { _ in - if let strongSelf = self { - strongSelf.item = item - strongSelf.layoutParams = params - - if let _ = updatedTheme { - strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor - strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor - } - - let (textLayout, textApply) = textApply - let textNode = textApply() - if strongSelf.textNode == nil { - strongSelf.textNode = textNode - strongSelf.addSubnode(textNode) - } - - let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((44.0 - textLayout.size.height) / 2.0)), size: textLayout.size) - textNode.frame = textFrame - - let separatorHeight = UIScreenPixel - let topHighlightInset: CGFloat = (firstWithHeader || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight - - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) - strongSelf.separatorNode.isHidden = last - - strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) - } - }) - }) - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) - } - - override public func headers() -> [ListViewItemHeader]? { - if let item = self.item { - return item.header.flatMap { [$0] } - } else { - return nil - } - } - - override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { - super.updateRevealOffset(offset: offset, transition: transition) - - if let params = self.layoutParams, let textNode = self.textNode { - let leftInset: CGFloat = 15.0 + params.leftInset - - var textFrame = textNode.frame - textFrame.origin.x = leftInset + offset - transition.updateFrame(node: textNode, frame: textFrame) - } - } - - override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { - if let item = self.item { - switch option.key { - case RevealOptionKey.delete.rawValue: - item.deleted(item.query) - default: - break - } - } - self.setRevealOptionsOpened(false, animated: true) - self.revealOptionsInteractivelyClosed() - } -} diff --git a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift deleted file mode 100644 index 79ccb825de..0000000000 --- a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift +++ /dev/null @@ -1,545 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import SwiftSignalKit -import TelegramCore -import Display -import TelegramPresentationData -import AccountContext -import RadialStatusNode -import GalleryUI -import TelegramUniversalVideoContent -import GalleryUI - -class WebSearchVideoGalleryItem: GalleryItem { - var id: AnyHashable { - return self.index - } - - let index: Int - - let context: AccountContext - let presentationData: PresentationData - let result: ChatContextResult - let content: UniversalVideoContent - let controllerInteraction: WebSearchGalleryControllerInteraction? - - init(context: AccountContext, presentationData: PresentationData, index: Int, result: ChatContextResult, content: UniversalVideoContent, controllerInteraction: WebSearchGalleryControllerInteraction?) { - self.context = context - self.presentationData = presentationData - self.index = index - self.result = result - self.content = content - self.controllerInteraction = controllerInteraction - } - - func node(synchronous: Bool) -> GalleryItemNode { - let node = WebSearchVideoGalleryItemNode(context: self.context, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction) - node.setupItem(self) - return node - } - - func updateNode(node: GalleryItemNode, synchronous: Bool) { - if let node = node as? WebSearchVideoGalleryItemNode { - node.setupItem(self) - } - } - - func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { - return nil - } -} - -private struct FetchControls { - let fetch: () -> Void - let cancel: () -> Void -} - -final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { - private let context: AccountContext - private let strings: PresentationStrings - private let controllerInteraction: WebSearchGalleryControllerInteraction? - - fileprivate let _ready = Promise() - - private let footerContentNode: WebSearchGalleryFooterContentNode - - private var videoNode: UniversalVideoNode? - private let statusButtonNode: HighlightableButtonNode - private let statusNode: RadialStatusNode - - private var isCentral = false - private var validLayout: (ContainerViewLayout, CGFloat)? - private var didPause = false - private var isPaused = true - - private var requiresDownload = false - - var item: WebSearchVideoGalleryItem? - - private let statusDisposable = MetaDisposable() - - private let fetchDisposable = MetaDisposable() - private var fetchStatus: EngineMediaResource.FetchStatus? - private var fetchControls: FetchControls? - - var playbackCompleted: (() -> Void)? - - init(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchGalleryControllerInteraction?) { - self.context = context - self.strings = presentationData.strings - self.controllerInteraction = controllerInteraction - - self.footerContentNode = WebSearchGalleryFooterContentNode(context: context, presentationData: presentationData) - - self.statusButtonNode = HighlightableButtonNode() - self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) - - super.init() - - self.statusButtonNode.addSubnode(self.statusNode) - self.statusButtonNode.addTarget(self, action: #selector(statusButtonPressed), forControlEvents: .touchUpInside) - - self.addSubnode(self.statusButtonNode) - - self.footerContentNode.cancel = { - controllerInteraction?.dismiss(true) - } - self.footerContentNode.send = { [weak self] in - if let strongSelf = self, let item = strongSelf.item { - controllerInteraction?.send(item.result) - } - } - } - - deinit { - self.statusDisposable.dispose() - } - - @objc override func contentTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - if recognizer.state == .ended { - if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - if let item = self.item, let selectionState = item.controllerInteraction?.selectionState { - let legacyItem = legacyWebSearchItem(engine: item.context.engine, result: item.result) - selectionState.toggleItemSelection(legacyItem, success: nil) - } - case .doubleTap: - super.contentTap(recognizer) - default: - break - } - } - } - } - - override func ready() -> Signal { - return self._ready.get() - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - - self.validLayout = (layout, navigationBarHeight) - - let statusDiameter: CGFloat = 50.0 - let statusFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusDiameter) / 2.0), y: floor((layout.size.height - statusDiameter) / 2.0)), size: CGSize(width: statusDiameter, height: statusDiameter)) - transition.updateFrame(node: self.statusButtonNode, frame: statusFrame) - transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusFrame.size)) - } - - func setupItem(_ item: WebSearchVideoGalleryItem) { - if self.item?.content.id != item.content.id { - var isAnimated = false - var mediaResource: EngineMediaResource? - if let content = item.content as? NativeVideoContent { - isAnimated = content.fileReference.media.isAnimated - mediaResource = EngineMediaResource(content.fileReference.media.resource) - } - - if let videoNode = self.videoNode { - videoNode.canAttachContent = false - videoNode.removeFromSupernode() - } - - let mediaManager = item.context.sharedContext.mediaManager - - let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery) - let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0) - videoNode.updateLayout(size: videoSize, transition: .immediate) - self.videoNode = videoNode - videoNode.isUserInteractionEnabled = false - videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335) - videoNode.canAttachContent = true - - self.requiresDownload = true - var mediaFileStatus: Signal = .single(nil) - if let mediaResource = mediaResource { - mediaFileStatus = item.context.engine.resources.status(resource: mediaResource) - |> map(Optional.init) - } - - self.statusDisposable.set((combineLatest(videoNode.status, mediaFileStatus) - |> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in - if let strongSelf = self { - var initialBuffering = false - var isPaused = true - if let value = value { - if let zoomableContent = strongSelf.zoomableContent, !value.dimensions.width.isZero && !value.dimensions.height.isZero { - let videoSize = CGSize(width: value.dimensions.width * 2.0, height: value.dimensions.height * 2.0) - if !zoomableContent.0.equalTo(videoSize) { - strongSelf.zoomableContent = (videoSize, zoomableContent.1) - strongSelf.videoNode?.updateLayout(size: videoSize, transition: .immediate) - } - } - switch value.status { - case .playing: - isPaused = false - case let .buffering(_, whilePlaying, _, _): - initialBuffering = true - isPaused = !whilePlaying - var isStreaming = false - if let fetchStatus = strongSelf.fetchStatus { - switch fetchStatus { - case .Local: - break - default: - isStreaming = true - } - } - if let content = item.content as? NativeVideoContent, !isStreaming { - initialBuffering = false - if !content.enableSound { - isPaused = false - } - } - default: - if let content = item.content as? NativeVideoContent, !content.streamVideo.enabled { - if !content.enableSound { - isPaused = false - } - } - } - } - - var fetching = false - if initialBuffering { - strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: {}) - } else { - var state: RadialStatusNodeState = .none - - if let fetchStatus = fetchStatus { - if strongSelf.requiresDownload { - switch fetchStatus { - case let .Fetching(_, progress): - fetching = true - isPaused = true - state = .progress(color: .white, lineWidth: nil, value: CGFloat(max(0.027, progress)), cancelEnabled: false, animateRotation: true) - default: - break - } - } - } - strongSelf.statusNode.transitionToState(state, animated: false, completion: {}) - } - - strongSelf.isPaused = isPaused - strongSelf.fetchStatus = fetchStatus - - strongSelf.statusButtonNode.isHidden = !initialBuffering && !isPaused && !fetching - } - })) - - self.zoomableContent = (videoSize, videoNode) - - videoNode.playbackCompleted = { [weak videoNode] in - Queue.mainQueue().async { - if !isAnimated { - videoNode?.seek(0.0) - } - } - } - - self._ready.set(videoNode.ready) - } - - self.item = item - } - - override func centralityUpdated(isCentral: Bool) { - super.centralityUpdated(isCentral: isCentral) - - if self.isCentral != isCentral { - self.isCentral = isCentral - - if let videoNode = self.videoNode, videoNode.ownsContentNode { - if isCentral { - videoNode.play() - } else { - videoNode.pause() - } - } - } - } - - override func activateAsInitial() { - if self.isCentral { - self.videoNode?.play() - } - } - - override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - return - } - - if let node = node.0 as? OverlayMediaItemNode { - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - - videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) - - self.context.sharedContext.mediaManager.setOverlayVideoNode(nil) - } else { - var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) - let transformedCopyViewFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - - let surfaceCopyView = node.2().0! - let copyView = node.2().0! - - addToTransitionSurface(surfaceCopyView) - - var transformedSurfaceFrame: CGRect? - var transformedSurfaceFinalFrame: CGRect? - if let contentSurface = surfaceCopyView.superview { - transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) - transformedSurfaceFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface) - } - - if let transformedSurfaceFrame = transformedSurfaceFrame { - surfaceCopyView.frame = transformedSurfaceFrame - } - - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) - copyView.frame = transformedSelfFrame - - copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false) - - surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak copyView] _ in - copyView?.removeFromSuperview() - }) - let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) - - if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame { - surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in - surfaceCopyView?.removeFromSuperview() - }) - let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFinalFrame.size.height / transformedSurfaceFrame.size.height) - surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) - } - - videoNode.allowsGroupOpacity = true - videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in - videoNode?.allowsGroupOpacity = false - }) - videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) - - self.statusButtonNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusButtonNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - self.statusButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - self.statusButtonNode.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - } - } - - override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - completion() - return - } - - var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) - let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - - var positionCompleted = false - var boundsCompleted = false - var copyCompleted = false - - let copyView = node.2().0! - let surfaceCopyView = node.2().0! - - addToTransitionSurface(surfaceCopyView) - - var transformedSurfaceFrame: CGRect? - var transformedSurfaceCopyViewInitialFrame: CGRect? - if let contentSurface = surfaceCopyView.superview { - transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface) - transformedSurfaceCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface) - } - - self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) - copyView.frame = transformedSelfFrame - - let intermediateCompletion = { [weak copyView, weak surfaceCopyView] in - if positionCompleted && boundsCompleted && copyCompleted { - copyView?.removeFromSuperview() - surfaceCopyView?.removeFromSuperview() - completion() - } - } - - copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false) - surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - copyCompleted = true - intermediateCompletion() - }) - - if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedCopyViewInitialFrame = transformedSurfaceCopyViewInitialFrame { - surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSurfaceFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSurfaceFrame.size.height) - surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) - } - - videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - positionCompleted = true - intermediateCompletion() - }) - - videoNode.allowsGroupOpacity = true - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in - videoNode?.allowsGroupOpacity = false - }) - - self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - //positionCompleted = true - //intermediateCompletion() - }) - self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - - transformedFrame.origin = CGPoint() - - let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - boundsCompleted = true - intermediateCompletion() - }) - } - - func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) { - guard let videoNode = self.videoNode else { - completion() - return - } - - var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view) - let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview) - let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) - let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view) - let transformedSelfTargetSuperFrame = videoNode.view.convert(videoNode.view.bounds, to: node.view.superview) - - var positionCompleted = false - var boundsCompleted = false - var copyCompleted = false - var nodeCompleted = false - - let copyView = node.view.snapshotContentTree()! - - videoNode.isHidden = true - copyView.frame = transformedSelfFrame - - let intermediateCompletion = { [weak copyView] in - if positionCompleted && boundsCompleted && copyCompleted && nodeCompleted { - copyView?.removeFromSuperview() - completion() - } - } - - copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) - - copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) - copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - copyCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - positionCompleted = true - intermediateCompletion() - }) - - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - - self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - //positionCompleted = true - //intermediateCompletion() - }) - self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) - self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) - - transformedFrame.origin = CGPoint() - - let videoTransform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0) - videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: videoTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - boundsCompleted = true - intermediateCompletion() - }) - - let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0) - node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in - nodeCompleted = true - intermediateCompletion() - }) - } - - @objc func statusButtonPressed() { - if let videoNode = self.videoNode { - if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { - self.toggleControlsVisibility() - } - - if let fetchStatus = self.fetchStatus { - switch fetchStatus { - case .Local: - videoNode.togglePlayPause() - case .Remote, .Paused: - if self.requiresDownload { - self.fetchControls?.fetch() - } else { - videoNode.togglePlayPause() - } - case .Fetching: - self.fetchControls?.cancel() - } - } else { - videoNode.togglePlayPause() - } - } - } - - override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { - return .single((self.footerContentNode, nil)) - } -} diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index d0fdded6f1..47ee5b384d 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -79,6 +79,7 @@ public struct WebAppParameters { let forceHasSettings: Bool let fullSize: Bool let isFullscreen: Bool + let sameOrigin: Bool let appSettings: BotAppSettings? public init( @@ -97,6 +98,7 @@ public struct WebAppParameters { forceHasSettings: Bool, fullSize: Bool, isFullscreen: Bool = false, + sameOrigin: Bool = false, appSettings: BotAppSettings? = nil ) { self.source = source @@ -114,6 +116,7 @@ public struct WebAppParameters { self.forceHasSettings = forceHasSettings self.fullSize = fullSize || isFullscreen self.isFullscreen = isFullscreen + self.sameOrigin = sameOrigin self.appSettings = appSettings } } @@ -469,6 +472,10 @@ public final class WebAppController: ViewController, AttachmentContainable { super.didLoad() self.setupWebView() + if let pendingExternalUrl = self.controller?.pendingExternalUrl { + self.controller?.pendingExternalUrl = nil + self.loadExternal(url: pendingExternalUrl) + } guard let webView = self.webView else { return @@ -516,7 +523,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } #endif*/ - if !"".isEmpty { + if self.controller?.sameOrigin == true { self.webView?.bindTrustedOrigin(from: url) } else { self.webView?.setupEventProxySource() @@ -524,6 +531,13 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.load(URLRequest(url: url)) } + fileprivate func loadExternal(url: String) { + guard let parsedUrl = URL(string: url) else { + return + } + self.load(url: parsedUrl) + } + func setupWebView() { guard let controller = self.controller else { return @@ -555,6 +569,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } if let parsedUrl = URL(string: result.url) { strongSelf.queryId = result.queryId + strongSelf.controller?.sameOrigin = result.flags.contains(.sameOrigin) strongSelf.load(url: parsedUrl) } }) @@ -575,6 +590,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return } self.controller?.titleView?.title = WebAppTitle(title: botApp.title, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: controller.botVerified) + self.controller?.sameOrigin = result.flags.contains(.sameOrigin) self.load(url: parsedUrl) }) }) @@ -585,6 +601,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return } strongSelf.queryId = result.queryId + strongSelf.controller?.sameOrigin = result.flags.contains(.sameOrigin) strongSelf.load(url: parsedUrl) if let keepAliveSignal = result.keepAliveSignal { @@ -1245,8 +1262,7 @@ public final class WebAppController: ViewController, AttachmentContainable { case "web_app_open_tg_link": if let json = json, let path = json["path_full"] as? String { let forceRequest = json["force_request"] as? Bool ?? false - controller.openUrl("https://t.me\(path)", false, forceRequest, { - }) + controller.openUrl("https://t.me\(path)", false, forceRequest, {}) } case "web_app_open_invoice": if let json = json, let slug = json["slug"] as? String { @@ -3552,6 +3568,8 @@ public final class WebAppController: ViewController, AttachmentContainable { private let replyToMessageId: EngineMessage.Id? private let threadId: Int64? public var isFullscreen: Bool + private var sameOrigin: Bool + private var pendingExternalUrl: String? private var presentationData: PresentationData fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? @@ -3585,6 +3603,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.replyToMessageId = replyToMessageId self.threadId = threadId self.isFullscreen = params.isFullscreen + self.sameOrigin = params.sameOrigin self.updatedPresentationData = updatedPresentationData @@ -3606,6 +3625,8 @@ public final class WebAppController: ViewController, AttachmentContainable { if case .attachMenu = self.source { + } else if self.isVerifyAgeBot { + } else { self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode) self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed) @@ -3683,15 +3704,21 @@ public final class WebAppController: ViewController, AttachmentContainable { } private func updateNavigationButtons() { + var showGlassButtons = false if case .attachMenu = self.source { + showGlassButtons = true + } else if self.isVerifyAgeBot { + showGlassButtons = true + } + if showGlassButtons { let barButtonSize = CGSize(width: 44.0, height: 44.0) let closeComponent: AnyComponentWithIdentity = AnyComponentWithIdentity( id: "close", component: AnyComponent(GlassBarButtonComponent( size: barButtonSize, - backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: self.presentationData.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: self.controllerNode.hasBackButton ? "back" : "close", component: AnyComponent( BundleIconComponent( name: self.controllerNode.hasBackButton ? "Navigation/Back" : "Navigation/Close", @@ -3708,9 +3735,9 @@ public final class WebAppController: ViewController, AttachmentContainable { id: "more", component: AnyComponent(GlassBarButtonComponent( size: barButtonSize, - backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: self.presentationData.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "more", component: AnyComponent( LottieComponent( content: LottieComponent.AppBundleContent( @@ -3738,14 +3765,16 @@ public final class WebAppController: ViewController, AttachmentContainable { self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: cancelButtonNode) } - let moreButtonNode: BarComponentHostNode - if let current = self.moreBarButtonNode { - moreButtonNode = current - moreButtonNode.component = moreComponent - } else { - moreButtonNode = BarComponentHostNode(component: moreComponent, size: barButtonSize) - self.moreBarButtonNode = moreButtonNode - self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: moreButtonNode) + if !self.isVerifyAgeBot { + let moreButtonNode: BarComponentHostNode + if let current = self.moreBarButtonNode { + moreButtonNode = current + moreButtonNode.component = moreComponent + } else { + moreButtonNode = BarComponentHostNode(component: moreComponent, size: barButtonSize) + self.moreBarButtonNode = moreButtonNode + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: moreButtonNode) + } } } @@ -3754,7 +3783,11 @@ public final class WebAppController: ViewController, AttachmentContainable { private var isVerifyAgeBot: Bool { if let ageBotUsername = self.context.currentAppConfiguration.with({ $0 }).data?["verify_age_bot_username"] as? String { - return self.botAddress == ageBotUsername + if self.botAddress == ageBotUsername { + return true + } else if self.botAddress == "mod_bot" { + return true + } } return false } @@ -4059,6 +4092,14 @@ public final class WebAppController: ViewController, AttachmentContainable { self.updateTabBarAlpha(1.0, .immediate) } + public func loadExternal(url: String) { + if self.isNodeLoaded { + self.controllerNode.loadExternal(url: url) + } else { + self.pendingExternalUrl = url + } + } + public func isContainerPanningUpdated(_ isPanning: Bool) { self.controllerNode.isContainerPanningUpdated(isPanning) } @@ -4102,23 +4143,18 @@ public final class WebAppController: ViewController, AttachmentContainable { public func requestDismiss(completion: @escaping () -> Void) { if self.controllerNode.needDismissConfirmation { - let actionSheet = ActionSheetController(presentationData: self.presentationData) - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: self.presentationData.strings.WebApp_CloseConfirmation), - ActionSheetButtonItem(title: self.presentationData.strings.WebApp_CloseAnyway, color: .destructive, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - + let alertController = textAlertController( + context: self.context, + title: nil, + text: self.presentationData.strings.WebApp_CloseConfirmation, + actions: [ + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.WebApp_CloseAnyway, action: { completion() }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) - self.present(actionSheet, in: .window(.root)) + ] + ) + self.present(alertController, in: .window(.root)) } else { completion() } @@ -4253,7 +4289,8 @@ public func standaloneWebAppController( didDismiss: @escaping () -> Void = {}, getNavigationController: @escaping () -> NavigationController? = { return nil }, getSourceRect: (() -> CGRect?)? = nil, - verifyAgeCompletion: ((Int) -> Void)? = nil + verifyAgeCompletion: ((Int) -> Void)? = nil, + onControllerCreated: @escaping (WebAppController) -> Void = { _ in } ) -> ViewController { let controller = AttachmentController( context: context, @@ -4274,6 +4311,7 @@ public func standaloneWebAppController( webAppController.getNavigationController = getNavigationController webAppController.requestSwitchInline = requestSwitchInline webAppController.verifyAgeCompletion = verifyAgeCompletion + onControllerCreated(webAppController) present(webAppController, webAppController.mediaPickerContext) return true } 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, diff --git a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift index 5550078d1c..0f06104c94 100644 --- a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift +++ b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift @@ -222,8 +222,6 @@ private final class SheetContent: CombinedComponent { transition: .immediate ) context.add(button - .clipsToBounds(true) - .cornerRadius(10.0) .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) ) contentSize.height += button.size.height diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index acebade7a2..de27125c74 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -70,7 +70,7 @@ private func securedEventProxySource(trustedOrigin: String) -> String { """ } -private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([\"contenteditable\"=\"true\"]){-webkit-user-select:none;}';" +private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([contenteditable=\"true\"]){-webkit-user-select:none;}';" + " var head = document.head || document.getElementsByTagName('head')[0];" + " var style = document.createElement('style'); style.type = 'text/css';" + " style.appendChild(document.createTextNode(css)); head.appendChild(style);"