diff --git a/Telegram/Telegram-iOS/Base.lproj/LaunchScreen.xib b/Telegram/Telegram-iOS/Base.lproj/LaunchScreen.xib index 3b02891e01..2e8de3f955 100644 --- a/Telegram/Telegram-iOS/Base.lproj/LaunchScreen.xib +++ b/Telegram/Telegram-iOS/Base.lproj/LaunchScreen.xib @@ -1,19 +1,38 @@ - + - - + + - + - + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 6a98e350f4..7ad8d647a2 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8624,3 +8624,69 @@ Sorry for the inconvenience."; "StorageManagement.OpenFile" = "Open File"; "ChatListFilter.AddChatsSearchPlaceholder" = "Search Chats"; + +"RequestPeer.ChooseUserTitle" = "Choose User"; +"RequestPeer.ChooseBotTitle" = "Choose Bot"; +"RequestPeer.ChooseGroupTitle" = "Choose Group"; +"RequestPeer.ChooseChannelTitle" = "Choose Channel"; +"RequestPeer.Requirements" = "Requirements:"; + +"RequestPeer.CreateNewGroup" = "Create a New Group for This"; +"RequestPeer.CreateNewChannel" = "Create a New Channel for This"; + +"RequestPeer.UsersEmpty" = "You don't have users that meet the following requirements:"; +"RequestPeer.UsersAllEmpty" = "You don't have any users."; +"RequestPeer.BotsAllEmpty" = "You don't have any bots."; +"RequestPeer.GroupsEmpty" = "You don't have groups that meet the following requirements:"; +"RequestPeer.GroupsAllEmpty" = "You don't have any groups."; +"RequestPeer.ChannelsEmpty" = "You don't have channels that meet the following requirements:"; +"RequestPeer.ChannelsAllEmpty" = "You don't have any channels."; + +"RequestPeer.Requirement.UserPremiumOff" = "User should not have a Premium subscription."; +"RequestPeer.Requirement.UserPremiumOn" = "User should have a Premium subscription."; + +"RequestPeer.Requirement.Group.HasUsernameOff" = "The group should be private."; +"RequestPeer.Requirement.Group.HasUsernameOn" = "The group should be public."; + +"RequestPeer.Requirement.Group.ForumOff" = "The group should have topics off."; +"RequestPeer.Requirement.Group.ForumOn" = "The group should have topics on."; + +"RequestPeer.Requirement.Group.CreatorOn" = "You should be the owner of the group."; + +"RequestPeer.Requirement.Group.Rights" = "You have the admin rights to %@."; +"RequestPeer.Requirement.Group.Rights.Info" = "change group info"; +"RequestPeer.Requirement.Group.Rights.Send" = "post messages"; +"RequestPeer.Requirement.Group.Rights.Delete" = "delete messages"; +"RequestPeer.Requirement.Group.Rights.Edit" = "delete messages"; +"RequestPeer.Requirement.Group.Rights.Ban" = "ban users"; +"RequestPeer.Requirement.Group.Rights.Invite" = "invite users via link"; +"RequestPeer.Requirement.Group.Rights.Pin" = "pin messages"; +"RequestPeer.Requirement.Group.Rights.Topics" = "manage topics"; +"RequestPeer.Requirement.Group.Rights.VideoChats" = "manage video chats"; +"RequestPeer.Requirement.Group.Rights.Anonymous" = "remain anonymous"; +"RequestPeer.Requirement.Group.Rights.AddAdmins" = "add new admins"; + +"RequestPeer.Requirement.Channel.HasUsernameOff" = "The channel should be private."; +"RequestPeer.Requirement.Channel.HasUsernameOn" = "The channel should be public."; + +"RequestPeer.Requirement.Channel.CreatorOn" = "You should be the owner of the channel."; + +"RequestPeer.Requirement.Channel.Rights" = "You have the admin rights to %@."; +"RequestPeer.Requirement.Channel.Rights.Info" = "change group info"; +"RequestPeer.Requirement.Channel.Rights.Send" = "post messages"; +"RequestPeer.Requirement.Channel.Rights.Delete" = "delete messages"; +"RequestPeer.Requirement.Channel.Rights.Edit" = "delete messages"; +"RequestPeer.Requirement.Channel.Rights.Ban" = "ban users"; +"RequestPeer.Requirement.Channel.Rights.Invite" = "invite users via link"; +"RequestPeer.Requirement.Channel.Rights.Pin" = "pin messages"; +"RequestPeer.Requirement.Channel.Rights.Topics" = "manage topics"; +"RequestPeer.Requirement.Channel.Rights.VideoChats" = "manage video chats"; +"RequestPeer.Requirement.Channel.Rights.Anonymous" = "remain anonymous"; +"RequestPeer.Requirement.Channel.Rights.AddAdmins" = "add new admins"; + +"CreateGroup.PublicLinkTitle" = "SET A PUBLIC LINK"; +"CreateGroup.PublicLinkInfo" = "You can use **a-z**, **0-9** and underscores. Minimum length is **5** characters."; + +"Notification.RequestedPeer" = "You shared %@ with the bot."; + +"Conversation.ViewInChannel" = "View in Channel"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 7b5e0b4fc9..7948b075a2 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -719,6 +719,7 @@ public enum CreateGroupMode { case generic case supergroup case locatedGroup(latitude: Double, longitude: Double, address: String?) + case requestPeer(ReplyMarkupButtonRequestPeerType.Group) } public protocol AppLockContext: AnyObject { diff --git a/submodules/AccountContext/Sources/PeerSelectionController.swift b/submodules/AccountContext/Sources/PeerSelectionController.swift index 4619663321..6c5efa1bfc 100644 --- a/submodules/AccountContext/Sources/PeerSelectionController.swift +++ b/submodules/AccountContext/Sources/PeerSelectionController.swift @@ -41,6 +41,7 @@ public final class PeerSelectionControllerParams { public let context: AccountContext public let updatedPresentationData: (initial: PresentationData, signal: Signal)? public let filter: ChatListNodePeersFilter + public let requestPeerType: ReplyMarkupButtonRequestPeerType? public let forumPeerId: EnginePeer.Id? public let hasChatListSelector: Bool public let hasContactSelector: Bool @@ -54,10 +55,11 @@ public final class PeerSelectionControllerParams { public let hasTypeHeaders: Bool public let selectForumThreads: Bool - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false, selectForumThreads: Bool = false) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], requestPeerType: ReplyMarkupButtonRequestPeerType? = nil, forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false, selectForumThreads: Bool = false) { self.context = context self.updatedPresentationData = updatedPresentationData self.filter = filter + self.requestPeerType = requestPeerType self.forumPeerId = forumPeerId self.hasChatListSelector = hasChatListSelector self.hasContactSelector = hasContactSelector diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 4c68f31f3d..27acb53b07 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -1117,7 +1117,10 @@ public final class AuthorizationSequenceController: NavigationController, MFMail let wasEmpty = self.viewControllers.isEmpty super.setViewControllers(viewControllers, animated: animated) if wasEmpty { - self.topViewController?.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + if self.topViewController is AuthorizationSequenceSplashController { + } else { + self.topViewController?.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } } if !self.didSetReady { self.didSetReady = true @@ -1150,7 +1153,13 @@ public final class AuthorizationSequenceController: NavigationController, MFMail } private func animateIn() { - self.view.layer.animatePosition(from: CGPoint(x: self.view.layer.position.x, y: self.view.layer.position.y + self.view.layer.bounds.size.height), to: self.view.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + if !self.otherAccountPhoneNumbers.1.isEmpty { + self.view.layer.animatePosition(from: CGPoint(x: self.view.layer.position.x, y: self.view.layer.position.y + self.view.layer.bounds.size.height), to: self.view.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } else { + if let splashController = self.topViewController as? AuthorizationSequenceSplashController { + splashController.animateIn() + } + } } private func animateOut(completion: (() -> Void)? = nil) { diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift index 823e0ee6b8..90eaee692c 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift @@ -10,7 +10,7 @@ import LegacyComponents import SolidRoundedButtonNode import RMIntro -final class AuthorizationSequenceSplashController: ViewController { +public final class AuthorizationSequenceSplashController: ViewController { private var controllerNode: AuthorizationSequenceSplashControllerNode { return self.displayNode as! AuthorizationSequenceSplashControllerNode } @@ -107,11 +107,15 @@ final class AuthorizationSequenceSplashController: ViewController { self.activateLocalizationDisposable.dispose() } - override public func loadDisplayNode() { + public override func loadDisplayNode() { self.displayNode = AuthorizationSequenceSplashControllerNode(theme: self.theme) self.displayNodeDidLoad() } + func animateIn() { + self.controller.animateIn() + } + var buttonFrame: CGRect { return self.startButton.frame } @@ -138,31 +142,31 @@ final class AuthorizationSequenceSplashController: ViewController { } } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.addControllerIfNeeded() self.controller.viewWillAppear(false) } - override func viewDidAppear(_ animated: Bool) { + public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) controller.viewDidAppear(animated) } - override func viewWillDisappear(_ animated: Bool) { + public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) controller.viewWillDisappear(animated) } - override func viewDidDisappear(_ animated: Bool) { + public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) controller.viewDidDisappear(animated) } - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.validLayout = layout diff --git a/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift b/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift index c55120dd20..045d8ac679 100644 --- a/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift +++ b/submodules/ChatListUI/Sources/ChatListAdditionalCategoryItem.swift @@ -121,7 +121,9 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade } } else if let _ = nextItem as? ChatListAdditionalCategoryItem { } else { - last = true + if let nextItem = nextItem as? ListViewItemWithHeader, nextItem.header != nil { + last = true + } } } else { last = true diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 3097a0f25d..2efd04a75b 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1535,7 +1535,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { //filter.insert(.excludeRecent) } - let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, location: effectiveLocation, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in + let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, requestPeerType: nil, location: effectiveLocation, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in self?.requestOpenPeerFromSearch?(peer, threadId, dismissSearch) }, openDisabledPeer: { _, _ in }, openRecentPeerOptions: { [weak self] peer in diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 8358be449d..9d68e005f5 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -86,6 +86,7 @@ private struct ChatListSearchContainerNodeSearchState: Equatable { public final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private let context: AccountContext private let peersFilter: ChatListNodePeersFilter + private let requestPeerType: ReplyMarkupButtonRequestPeerType? private let location: ChatListControllerLocation private let displaySearchFilters: Bool private let hasDownloads: Bool @@ -135,7 +136,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private var validLayout: (ContainerViewLayout, CGFloat)? - public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { + public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, filter: ChatListNodePeersFilter, requestPeerType: ReplyMarkupButtonRequestPeerType?, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) { var initialFilter = initialFilter if case .chats = initialFilter, case .forum = location { initialFilter = .topics @@ -143,6 +144,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.context = context self.peersFilter = filter + self.requestPeerType = requestPeerType self.location = location self.displaySearchFilters = displaySearchFilters self.hasDownloads = hasDownloads @@ -160,7 +162,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) self.filterContainerNode = ChatListSearchFiltersContainerNode() - self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) + self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController) self.paneContainerNode.clipsToBounds = true super.init() diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index db6e7b934d..5e60c52c7e 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -933,6 +933,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let animationRenderer: MultiAnimationRenderer private let interaction: ChatListSearchInteraction private let peersFilter: ChatListNodePeersFilter + private let requestPeerType: ReplyMarkupButtonRequestPeerType? private var presentationData: PresentationData private let key: ChatListSearchPaneKey private let tagMask: EngineMessage.Tags? @@ -981,7 +982,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private let emptyResultsTitleNode: ImmediateTextNode private let emptyResultsTextNode: ImmediateTextNode private let emptyResultsAnimationNode: AnimatedStickerNode - private var emptyResultsAnimationSize: CGSize = CGSize() + private var emptyResultsAnimationSize = CGSize() private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData)? @@ -1002,7 +1003,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var hiddenMediaDisposable: Disposable? - init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: ReplyMarkupButtonRequestPeerType?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer @@ -1018,6 +1019,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { peersFilter.insert(.excludeRecent) } self.peersFilter = peersFilter + self.requestPeerType = requestPeerType let tagMask: EngineMessage.Tags? switch key { @@ -1645,33 +1647,124 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let _ = currentRemotePeers.swap((foundRemotePeers.0, foundRemotePeers.1)) let filteredPeer: (EnginePeer, EnginePeer) -> Bool = { peer, accountPeer in - guard !peersFilter.contains(.excludeSavedMessages) || peer.id != accountPeer.id else { return false } - guard !peersFilter.contains(.excludeSecretChats) || peer.id.namespace != Namespaces.Peer.SecretChat else { return false } - guard !peersFilter.contains(.onlyPrivateChats) || peer.id.namespace == Namespaces.Peer.CloudUser else { return false } - - if peersFilter.contains(.onlyGroups) { - var isGroup: Bool = false - if case let .channel(peer) = peer, case .group = peer.info { - isGroup = true - } else if peer.id.namespace == Namespaces.Peer.CloudGroup { - isGroup = true + if let peerType = requestPeerType { + switch peerType { + case let .user(userType): + if case let .user(user) = peer { + if let isBot = userType.isBot { + if isBot != (user.botInfo != nil) { + return false + } + } + if let isPremium = userType.isPremium { + if isPremium != user.isPremium { + return false + } + } + return true + } else { + return false + } + case let .group(groupType): + if case let .legacyGroup(group) = peer { + if groupType.isCreator { + if case .creator = group.role { + } else { + return false + } + } + if let isForum = groupType.isForum, isForum { + return false + } + if let hasUsername = groupType.hasUsername, hasUsername { + return false + } + if let userAdminRights = groupType.userAdminRights { + if case let .admin(rights, _) = group.role { + if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights { + return false + } + } else if case .member = group.role { + return false + } + } + return true + } else if case let .channel(channel) = peer, case .group = channel.info { + if groupType.isCreator { + if !channel.flags.contains(.isCreator) { + return false + } + } + if let isForum = groupType.isForum, isForum { + if isForum != channel.flags.contains(.isForum) { + return false + } + } + if let hasUsername = groupType.hasUsername, hasUsername { + if hasUsername != (channel.addressName != nil) { + return false + } + } + if let userAdminRights = groupType.userAdminRights { + if channel.flags.contains(.isCreator) { + } else if let rights = channel.adminRights { + if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights { + return false + } + } else { + return false + } + } + return true + } else { + return false + } + case let .channel(channelType): + if case let .channel(channel) = peer, case .broadcast = channel.info { + if channelType.isCreator { + if !channel.flags.contains(.isCreator) { + return false + } + } + if let hasUsername = channelType.hasUsername, hasUsername { + if hasUsername != (channel.addressName != nil) { + return false + } + } + return true + } else { + return false + } } - if !isGroup { - return false + } else { + guard !peersFilter.contains(.excludeSavedMessages) || peer.id != accountPeer.id else { return false } + guard !peersFilter.contains(.excludeSecretChats) || peer.id.namespace != Namespaces.Peer.SecretChat else { return false } + guard !peersFilter.contains(.onlyPrivateChats) || peer.id.namespace == Namespaces.Peer.CloudUser else { return false } + + if peersFilter.contains(.onlyGroups) { + var isGroup: Bool = false + if case let .channel(peer) = peer, case .group = peer.info { + isGroup = true + } else if peer.id.namespace == Namespaces.Peer.CloudGroup { + isGroup = true + } + if !isGroup { + return false + } } - } - - if peersFilter.contains(.onlyChannels) { - if case let .channel(peer) = peer, case .broadcast = peer.info { - return true - } else { - return false + + if peersFilter.contains(.onlyChannels) { + if case let .channel(peer) = peer, case .broadcast = peer.info { + return true + } else { + return false + } } - } - - if peersFilter.contains(.excludeChannels) { - if case let .channel(peer) = peer, case .broadcast = peer.info { - return false + + if peersFilter.contains(.excludeChannels) { + if case let .channel(peer) = peer, case .broadcast = peer.info { + return false + } } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 795ee660c9..30dcddb37a 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -119,13 +119,14 @@ private final class ChatListSearchPendingPane { interaction: ChatListSearchInteraction, navigationController: NavigationController?, peersFilter: ChatListNodePeersFilter, + requestPeerType: ReplyMarkupButtonRequestPeerType?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, key: ChatListSearchPaneKey, hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void ) { - let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) + let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController) self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady @@ -147,6 +148,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD private let animationRenderer: MultiAnimationRenderer private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let peersFilter: ChatListNodePeersFilter + private let requestPeerType: ReplyMarkupButtonRequestPeerType? private let location: ChatListControllerLocation private let searchQuery: Signal private let searchOptions: Signal @@ -181,12 +183,13 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD private var currentAvailablePanes: [ChatListSearchPaneKey]? - init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { + init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peersFilter: ChatListNodePeersFilter, requestPeerType: ReplyMarkupButtonRequestPeerType?, location: ChatListControllerLocation, searchQuery: Signal, searchOptions: Signal, navigationController: NavigationController?) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer self.updatedPresentationData = updatedPresentationData self.peersFilter = peersFilter + self.requestPeerType = requestPeerType self.location = location self.searchQuery = searchQuery self.searchOptions = searchOptions @@ -420,6 +423,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD interaction: self.interaction!, navigationController: self.navigationController, peersFilter: self.peersFilter, + requestPeerType: self.requestPeerType, location: self.location, searchQuery: self.searchQuery, searchOptions: self.searchOptions, diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 045bc6ba5e..030a2e7c31 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -21,6 +21,7 @@ import Postbox public enum ChatListNodeMode { case chatList case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool) + case peerType(type: ReplyMarkupButtonRequestPeerType) } struct ChatListNodeListViewTransition { @@ -307,9 +308,12 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData): var header: ChatListSearchItemHeader? - if case .action = appearance { - // TODO: hack, generalize - header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + if case .peerType = mode { + } else { + if case .action = appearance { + // TODO: hack, generalize + header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + } } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), @@ -546,6 +550,37 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) + case .peerType: + let itemPeer = peer.chatMainPeer + var chatPeer: EnginePeer? + if let peer = peer.peers[peer.peerId] { + chatPeer = peer + } + + let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer) + let status: ContactsPeerItemStatus = .none + + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), + sortOrder: presentationData.nameSortOrder, + displayOrder: presentationData.nameDisplayOrder, + context: context, + peerMode: .generalSearch, + peer: peerContent, + status: status, + enabled: true, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + index: nil, + header: nil, + action: { _ in + if let chatPeer = chatPeer { + nodeInteraction.peerSelected(chatPeer, nil, nil, nil) + } + }, disabledAction: nil, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer + ), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) @@ -766,6 +801,37 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL animationCache: nodeInteraction.animationCache, animationRenderer: nodeInteraction.animationRenderer ), directionHint: entry.directionHint) + case .peerType: + let itemPeer = peer.chatMainPeer + var chatPeer: EnginePeer? + if let peer = peer.peers[peer.peerId] { + chatPeer = peer + } + + let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer) + let status: ContactsPeerItemStatus = .none + + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem( + presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), + sortOrder: presentationData.nameSortOrder, + displayOrder: presentationData.nameDisplayOrder, + context: context, + peerMode: .generalSearch, + peer: peerContent, + status: status, + enabled: true, + selection: .none, + editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), + index: nil, + header: nil, + action: { _ in + if let chatPeer = chatPeer { + nodeInteraction.peerSelected(chatPeer, nil, nil, nil) + } + }, disabledAction: nil, + animationCache: nodeInteraction.animationCache, + animationRenderer: nodeInteraction.animationRenderer + ), directionHint: entry.directionHint) } case let .HoleEntry(_, theme): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) @@ -806,9 +872,12 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData): var header: ChatListSearchItemHeader? - if case .action = appearance { - // TODO: hack, generalize - header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + if case .peerType = mode { + } else { + if case .action = appearance { + // TODO: hack, generalize + header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) + } } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), @@ -963,7 +1032,7 @@ public final class ChatListNode: ListView { public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? public var contentScrollingEnded: ((ListView) -> Bool)? - var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)? + public var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)? private var currentIsEmptyState: ChatListNodeEmptyState? public var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? @@ -1491,13 +1560,15 @@ public final class ChatListNode: ListView { let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, storageInfo: storageInfo, suggestPasswordSetup: suggestPasswordSetup, mode: mode, chatListLocation: location) - let entries = rawEntries.filter { entry in + var isEmpty = true + var entries = rawEntries.filter { entry in switch entry { case let .PeerEntry(peerEntry): let peer = peerEntry.peer switch mode { case .chatList: + isEmpty = false return true case let .peers(filter, _, _, _, _): guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false } @@ -1601,12 +1672,113 @@ public final class ChatListNode: ListView { } } + isEmpty = false return true + case let .peerType(peerType): + if let peer = peer.peer { + switch peerType { + case let .user(userType): + if case let .user(user) = peer { + if let isBot = userType.isBot { + if isBot != (user.botInfo != nil) { + return false + } + } + if let isPremium = userType.isPremium { + if isPremium != user.isPremium { + return false + } + } + isEmpty = false + return true + } else { + return false + } + case let .group(groupType): + if case let .legacyGroup(group) = peer { + if groupType.isCreator { + if case .creator = group.role { + } else { + return false + } + } + if let isForum = groupType.isForum, isForum { + return false + } + if let hasUsername = groupType.hasUsername, hasUsername { + return false + } + if let userAdminRights = groupType.userAdminRights { + if case let .admin(rights, _) = group.role { + if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights { + return false + } + } else if case .member = group.role { + return false + } + } + isEmpty = false + return true + } else if case let .channel(channel) = peer, case .group = channel.info { + if groupType.isCreator { + if !channel.flags.contains(.isCreator) { + return false + } + } + if let isForum = groupType.isForum, isForum { + if isForum != channel.flags.contains(.isForum) { + return false + } + } + if let hasUsername = groupType.hasUsername, hasUsername { + if hasUsername != (channel.addressName != nil) { + return false + } + } + if let userAdminRights = groupType.userAdminRights { + if channel.flags.contains(.isCreator) { + } else if let rights = channel.adminRights { + if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights { + return false + } + } else { + return false + } + } + isEmpty = false + return true + } else { + return false + } + case let .channel(channelType): + if case let .channel(channel) = peer, case .broadcast = channel.info { + if channelType.isCreator { + if !channel.flags.contains(.isCreator) { + return false + } + } + if let hasUsername = channelType.hasUsername, hasUsername { + if hasUsername != (channel.addressName != nil) { + return false + } + } + isEmpty = false + return true + } else { + return false + } + } + } else { + return false + } } default: return true } } + if isEmpty { + entries = [] + } let processedView = ChatListNodeView(originalList: update.list, filteredEntries: entries, isLoading: isLoading, filter: filter) let previousView = previousView.swap(processedView) @@ -1849,7 +2021,7 @@ public final class ChatListNode: ListView { switch mode { case .chatList: initialLocation = .initial(count: 50, filter: self.chatListFilter) - case .peers: + case .peers, .peerType: initialLocation = .initial(count: 200, filter: self.chatListFilter) } self.setChatListLocation(initialLocation) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 271181d2b5..ff977df0ac 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -675,11 +675,22 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState result.append(.HeaderEntry) } - if !view.hasLater, case let .peers(_, _, additionalCategories, _, _) = mode { - var index = 0 - for category in additionalCategories.reversed() { - result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData)) - index += 1 + if !view.hasLater { + if case let .peers(_, _, additionalCategories, _, _) = mode { + var index = 0 + for category in additionalCategories.reversed() { + result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData)) + index += 1 + } + } else if case let .peerType(type) = mode, !result.isEmpty { + switch type { + case .group: + result.append(.AdditionalCategory(index: 0, id: 0, title: "Create a New Group for This", image: PresentationResourcesItemList.createGroupIcon(state.presentationData.theme), appearance: .action, selected: false, presentationData: state.presentationData)) + case .channel: + result.append(.AdditionalCategory(index: 0, id: 0, title: "Create a New Channel for This", image: PresentationResourcesItemList.createGroupIcon(state.presentationData.theme), appearance: .action, selected: false, presentationData: state.presentationData)) + default: + break + } } } } diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index b0454a7af2..ac7fce4146 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -345,7 +345,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { Queue.concurrentDefaultQueue().async { if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) { - strongSelf.recognitionDisposable.set((recognizedContent(engine: strongSelf.context.engine, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id) + strongSelf.recognitionDisposable.set((recognizedContent(context: strongSelf.context, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id) |> deliverOnMainQueue).start(next: { [weak self] results in if let strongSelf = self { strongSelf.recognizedContentNode?.removeFromSupernode() diff --git a/submodules/ImageContentAnalysis/BUILD b/submodules/ImageContentAnalysis/BUILD index 07d864765b..cfcaa9efbb 100644 --- a/submodules/ImageContentAnalysis/BUILD +++ b/submodules/ImageContentAnalysis/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/AccountContext:AccountContext", ], visibility = [ "//visibility:public", diff --git a/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift b/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift index a910e80f66..b0ba996cad 100644 --- a/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift +++ b/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift @@ -5,6 +5,7 @@ import SwiftSignalKit import Postbox import TelegramCore import TelegramUIPreferences +import AccountContext private final class CachedImageRecognizedContent: Codable { public let results: [RecognizedContent] @@ -332,8 +333,11 @@ private func recognizeContent(in image: UIImage?) -> Signal<[RecognizedContent], } } -public func recognizedContent(engine: TelegramEngine, image: @escaping () -> UIImage?, messageId: MessageId) -> Signal<[RecognizedContent], NoError> { - return cachedImageRecognizedContent(engine: engine, messageId: messageId) +public func recognizedContent(context: AccountContext, image: @escaping () -> UIImage?, messageId: MessageId) -> Signal<[RecognizedContent], NoError> { + if context.sharedContext.immediateExperimentalUISettings.disableImageContentAnalysis { + return .single([]) + } + return cachedImageRecognizedContent(engine: context.engine, messageId: messageId) |> mapToSignal { cachedContent -> Signal<[RecognizedContent], NoError> in if let cachedContent = cachedContent { return .single(cachedContent.results) @@ -343,7 +347,7 @@ public func recognizedContent(engine: TelegramEngine, image: @escaping () -> UII |> then( recognizeContent(in: image()) |> beforeNext { results in - let _ = updateCachedImageRecognizedContent(engine: engine, messageId: messageId, content: CachedImageRecognizedContent(results: results)).start() + let _ = updateCachedImageRecognizedContent(engine: context.engine, messageId: messageId, content: CachedImageRecognizedContent(results: results)).start() } ) } diff --git a/submodules/RMIntro/PublicHeaders/RMIntro/RMIntroViewController.h b/submodules/RMIntro/PublicHeaders/RMIntro/RMIntroViewController.h index 2e0f0337c6..2ac83f5c81 100644 --- a/submodules/RMIntro/PublicHeaders/RMIntro/RMIntroViewController.h +++ b/submodules/RMIntro/PublicHeaders/RMIntro/RMIntroViewController.h @@ -66,6 +66,8 @@ - (UIView *)createAnimationSnapshot; - (UIView *)createTextSnapshot; +- (void)animateIn; + @property (nonatomic) bool isEnabled; - (void)startTimer; diff --git a/submodules/RMIntro/Sources/platform/ios/RMIntroPageView.h b/submodules/RMIntro/Sources/platform/ios/RMIntroPageView.h index a49c41c6e4..e2518343ae 100644 --- a/submodules/RMIntro/Sources/platform/ios/RMIntroPageView.h +++ b/submodules/RMIntro/Sources/platform/ios/RMIntroPageView.h @@ -14,6 +14,9 @@ NSMutableAttributedString *_description; } +@property (nonatomic, readonly) UILabel *headerLabel; +@property (nonatomic, readonly) UILabel *descriptionLabel; + - (id)initWithFrame:(CGRect)frame headline:(NSString*)headline description:(NSString*)description color:(UIColor *)color; @end diff --git a/submodules/RMIntro/Sources/platform/ios/RMIntroPageView.m b/submodules/RMIntro/Sources/platform/ios/RMIntroPageView.m index 259eed5e5c..75b978953c 100644 --- a/submodules/RMIntro/Sources/platform/ios/RMIntroPageView.m +++ b/submodules/RMIntro/Sources/platform/ios/RMIntroPageView.m @@ -29,7 +29,7 @@ headlineLabel.textColor = color; headlineLabel.textAlignment = NSTextAlignmentCenter; headlineLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth; - + _headerLabel = headlineLabel; NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; style.lineSpacing = IPAD ? 4 : 3; @@ -76,7 +76,7 @@ descriptionLabel.numberOfLines=0; descriptionLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth; [self addSubview:descriptionLabel]; - + _descriptionLabel = descriptionLabel; [self addSubview:headlineLabel]; diff --git a/submodules/RMIntro/Sources/platform/ios/RMIntroViewController.m b/submodules/RMIntro/Sources/platform/ios/RMIntroViewController.m index 8f2a8d80db..4e8be0d8df 100644 --- a/submodules/RMIntro/Sources/platform/ios/RMIntroViewController.m +++ b/submodules/RMIntro/Sources/platform/ios/RMIntroViewController.m @@ -102,6 +102,7 @@ typedef enum { NSDictionary *_englishStrings; UIView *_wrapperView; + UIView *_startButton; bool _loadedView; } @@ -123,9 +124,7 @@ typedef enum { _accentColor = accentColor; _regularDotColor = regularDotColor; _highlightedDotColor = highlightedDotColor; - - self.automaticallyAdjustsScrollViewInsets = false; - + NSArray *stringKeys = @[ @"Tour.Title1", @"Tour.Title2", @@ -227,6 +226,45 @@ typedef enum { } } +- (void)animateIn { + CGPoint logoTargetPosition = _glkView.center; + _glkView.center = CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0); + + RMIntroPageView *firstPage = (RMIntroPageView *)[_pageViews firstObject]; + CGPoint headerTargetPosition = firstPage.headerLabel.center; + firstPage.headerLabel.center = CGPointMake(headerTargetPosition.x, headerTargetPosition.y + 140.0); + + CGPoint descriptionTargetPosition = firstPage.descriptionLabel.center; + firstPage.descriptionLabel.center = CGPointMake(descriptionTargetPosition.x, descriptionTargetPosition.y + 160.0); + + CGPoint pageControlTargetPosition = _pageControl.center; + _pageControl.center = CGPointMake(pageControlTargetPosition.x, pageControlTargetPosition.y + 200.0); + + CGPoint buttonTargetPosition = _startButton.center; + _startButton.center = CGPointMake(buttonTargetPosition.x, buttonTargetPosition.y + 220.0); + + _glkView.transform = CGAffineTransformMakeScale(0.68, 0.68); + + [UIView animateWithDuration:0.65 delay:0.15 usingSpringWithDamping:1.2f initialSpringVelocity:0.0 options:kNilOptions animations:^{ + _glkView.center = logoTargetPosition; + firstPage.headerLabel.center = headerTargetPosition; + firstPage.descriptionLabel.center = descriptionTargetPosition; + _pageControl.center = pageControlTargetPosition; + _startButton.center = buttonTargetPosition; + _glkView.transform = CGAffineTransformIdentity; + } completion:nil]; + + _pageScrollView.alpha = 0.0; + _pageControl.alpha = 0.0; + _startButton.alpha = 0.0; + + [UIView animateWithDuration:0.3 delay:0.15 options:kNilOptions animations:^{ + _pageScrollView.alpha = 1.0; + _pageControl.alpha = 1.0; + _startButton.alpha = 1.0; + } completion:nil]; +} + - (void)loadGL { #if TARGET_OS_SIMULATOR && defined(__aarch64__) @@ -313,6 +351,7 @@ typedef enum { [self.view addSubview:_wrapperView]; _pageScrollView = [[UIScrollView alloc]initWithFrame:self.view.bounds]; + _pageScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; _pageScrollView.clipsToBounds = true; _pageScrollView.opaque = true; _pageScrollView.clearsContextBeforeDrawing = false; @@ -512,6 +551,7 @@ typedef enum { CGFloat startButtonWidth = MIN(430.0 - 48.0, self.view.bounds.size.width - 48.0f); UIView *startButton = self.createStartButton(startButtonWidth); if (startButton.superview == nil) { + _startButton = startButton; [self.view addSubview:startButton]; } startButton.frame = CGRectMake(floor((self.view.bounds.size.width - startButtonWidth) / 2.0f), self.view.bounds.size.height - startButtonY - statusBarHeight, startButtonWidth, 50.0f); @@ -523,10 +563,9 @@ typedef enum { _pageScrollView.contentSize=CGSizeMake(_headlines.count * self.view.bounds.size.width, 150); _pageScrollView.contentOffset = CGPointMake(_currentPage * self.view.bounds.size.width, 0); - [_pageViews enumerateObjectsUsingBlock:^(UIView *pageView, NSUInteger index, __unused BOOL *stop) - { - pageView.frame = CGRectMake(index * self.view.bounds.size.width, (pageY - statusBarHeight), self.view.bounds.size.width, 150); - }]; + [_pageViews enumerateObjectsUsingBlock:^(UIView *pageView, NSUInteger index, __unused BOOL *stop) { + pageView.frame = CGRectMake(index * self.view.bounds.size.width, (pageY - statusBarHeight), self.view.bounds.size.width, 150); + }]; } - (void)viewWillAppear:(BOOL)animated diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index c5241ff0a8..94617c7388 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -512,7 +512,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD let presentationData = context.sharedContext.currentPresentationData.with { $0 } var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, _ in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ViewInChannel, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, _ in c.dismiss(completion: { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { peer in diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index d2e5c8ec6e..4fbb91613c 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -834,9 +834,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, case .attachMenuBotAllowed: attributedString = NSAttributedString(string: strings.Notification_BotWriteAllowed, font: titleFont, textColor: primaryTextColor) case let .requestedPeer(_, peerId): - //TODO:localize - let _ = peerId - attributedString = NSAttributedString(string: "Requested Chat", font: titleFont, textColor: primaryTextColor) + let peerName = message.peers[peerId].flatMap(EnginePeer.init)?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? "" + attributedString = addAttributesToStringWithRanges(strings.Notification_RequestedPeer(peerName)._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, peerId)])) case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index fb85fd20e7..99ef0900e6 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -167,6 +167,7 @@ public final class ChatControllerInteraction { public let openJoinLink: (String) -> Void public let openWebView: (String, String, Bool, Bool) -> Void public let activateAdAction: (EngineMessage.Id) -> Void + public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32) -> Void public let requestMessageUpdate: (MessageId, Bool) -> Void public let cancelInteractiveKeyboardGestures: () -> Void @@ -277,6 +278,7 @@ public final class ChatControllerInteraction { openJoinLink: @escaping (String) -> Void, openWebView: @escaping (String, String, Bool, Bool) -> Void, activateAdAction: @escaping (EngineMessage.Id) -> Void, + openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32) -> Void, requestMessageUpdate: @escaping (MessageId, Bool) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, dismissTextInput: @escaping () -> Void, @@ -370,6 +372,7 @@ public final class ChatControllerInteraction { self.openJoinLink = openJoinLink self.openWebView = openWebView self.activateAdAction = activateAdAction + self.openRequestedPeerSelection = openRequestedPeerSelection self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures self.dismissTextInput = dismissTextInput diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 24f0c38359..01e8fab999 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -35,6 +35,7 @@ import BackgroundTasks import UIKitRuntimeUtils import StoreKit import PhoneNumberFormat +import AuthorizationUI #if canImport(AppCenter) import AppCenter @@ -284,7 +285,7 @@ private func extractAccountManagerState(records: AccountRecordsView Void)?, other: (() -> Void)?)? private let deviceToken = Promise(nil) - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { precondition(!testIsLaunched) testIsLaunched = true @@ -315,6 +316,11 @@ private func extractAccountManagerState(records: AccountRecordsView deliverOnMainQueue).start(next: { context in + |> deliverOnMainQueue).start(next: { [weak launchIconView] context in print("Application: context took \(CFAbsoluteTimeGetCurrent() - startTime) to become available") var network: Network? @@ -1075,8 +1081,15 @@ private func extractAccountManagerState(records: AccountRecordsView deliverOnMainQueue).start(next: { context in + |> deliverOnMainQueue).start(next: { [weak launchIconView] context in var network: Network? if let context = context { network = context.account.network @@ -1155,15 +1168,38 @@ private func extractAccountManagerState(records: AccountRecordsView { [weak self] subscriber in + let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + self?.mainWindow.present(statusController, on: .root) + return ActionDisposable { [weak statusController] in + Queue.mainQueue().async() { + statusController?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.5, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + let isReady: Signal = context.isReady.get() authContextReadyDisposable.set((isReady |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { _ in - statusController.dismiss() + progressDisposable.dispose() self.mainWindow.present(context.rootController, on: .root) + + if let launchIconView { + if context.rootController.topViewController is AuthorizationSequenceSplashController { + context.rootController.view.addSubview(launchIconView) + Queue.mainQueue().after(0.01, { + launchIconView.removeFromSuperview() + }) + } else { + launchIconView.removeFromSuperview() + } + } })) } else { authContextReadyDisposable.set(nil) diff --git a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift index 71c0242a64..aa18aab993 100644 --- a/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatButtonKeyboardInputNode.swift @@ -431,8 +431,10 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { }) case let .openWebView(url, simple): self.controllerInteraction.openWebView(markupButton.title, url, simple, false) - case .requestPeer: - break + case let .requestPeer(peerType, buttonId): + if let message = self.message { + self.controllerInteraction.openRequestedPeerSelection(message.id, peerType, buttonId) + } } if dismissIfOnce { if let message = self.message { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ee74217187..999612f0df 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4131,13 +4131,53 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .join(_, joinHash): self.controllerInteraction?.openJoinLink(joinHash) } + }, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId in + guard let self else { + return + } + let context = self.context + let peerId = self.chatLocation.peerId + var createNewGroupImpl: (() -> Void)? + let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent], requestPeerType: peerType, hasContactSelector: false, createNewGroup: { + createNewGroupImpl?() + })) + controller.peerSelected = { [weak controller] peer, _ in + let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peer.id).start() + + controller?.dismiss() + } + createNewGroupImpl = { [weak controller] in + switch peerType { + case .user: + break + case let .group(group): + let createGroupController = createGroupControllerImpl(context: context, peerIds: peerId.flatMap { [$0] } ?? [], mode: .requestPeer(group), completion: { peerId, dismiss in + let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peerId).start() + + dismiss() + }) + createGroupController.navigationPresentation = .modal + controller?.replace(with: createGroupController) + case let .channel(channel): + let createChannelController = createChannelController(context: context, mode: .requestPeer(channel), completion: { peerId, dismiss in + let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peerId).start() + + dismiss() + }) + createChannelController.navigationPresentation = .modal + controller?.replace(with: createChannelController) + } + } + self.push(controller) }, requestMessageUpdate: { [weak self] id, scroll in - if let strongSelf = self { - strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) + if let self { + self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) } }, cancelInteractiveKeyboardGestures: { [weak self] in - (self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() - self?.chatDisplayNode.cancelInteractiveKeyboardGestures() + if let self { + (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + self.chatDisplayNode.cancelInteractiveKeyboardGestures() + } }, dismissTextInput: { [weak self] in self?.chatDisplayNode.dismissTextInput() }, scrollToMessageId: { [weak self] index in diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index f6ca5dd45e..758a7cfe33 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1552,7 +1552,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } if isReplyThreadHead { - actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.SharedMedia_ViewInChat, icon: { theme in + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ViewInChannel, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.actionSheet.primaryTextColor) }, action: { c, _ in c.dismiss(completion: { diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 678e6c7887..5a14599c26 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -555,6 +555,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _ in + }, openRequestedPeerSelection: { _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index 7d2ac80b8d..93c39da1ec 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -206,7 +206,13 @@ private func CreateChannelEntries(presentationData: PresentationData, state: Cre return entries } -public func createChannelController(context: AccountContext) -> ViewController { +public enum CreateChannelMode { + case generic + case requestPeer(ReplyMarkupButtonRequestPeerType.Channel) +} + + +public func createChannelController(context: AccountContext, mode: CreateChannelMode = .generic, completion: ((PeerId, @escaping () -> Void) -> Void)? = nil) -> ViewController { let initialState = CreateChannelState(creating: false, editingName: ItemListAvatarAndNameInfoItemName.title(title: "", type: .channel), editingDescriptionText: "", avatar: nil) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index d7cbef2a79..5dbcdc2683 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -29,6 +29,7 @@ import LegacyMediaPickerUI import ContextUI import ChatTimerScreen import AsyncDisplayKit +import TextFormat private struct CreateGroupArguments { let context: AccountContext @@ -39,10 +40,14 @@ private struct CreateGroupArguments { let changeLocation: () -> Void let updateWithVenue: (TelegramMediaMap) -> Void let updateAutoDelete: () -> Void + let updatePublicLinkText: (String) -> Void + let openAuction: (String) -> Void } private enum CreateGroupSection: Int32 { case info + case username + case topics case autoDelete case members case location @@ -65,6 +70,12 @@ private enum CreateGroupEntryTag: ItemListItemTag { private enum CreateGroupEntry: ItemListNodeEntry { case groupInfo(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?) case setProfilePhoto(PresentationTheme, String) + case usernameHeader(PresentationTheme, String) + case username(PresentationTheme, String, String) + case usernameStatus(PresentationTheme, String, AddressNameValidationStatus, String, String) + case usernameInfo(PresentationTheme, String) + case topics(PresentationTheme, String) + case topicsInfo(PresentationTheme, String) case autoDelete(title: String, value: String) case autoDeleteInfo(String) case member(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, PeerPresence?) @@ -79,6 +90,10 @@ private enum CreateGroupEntry: ItemListNodeEntry { switch self { case .groupInfo, .setProfilePhoto: return CreateGroupSection.info.rawValue + case .usernameHeader, .username, .usernameStatus, .usernameInfo: + return CreateGroupSection.username.rawValue + case .topics, .topicsInfo: + return CreateGroupSection.topics.rawValue case .autoDelete, .autoDeleteInfo: return CreateGroupSection.autoDelete.rawValue case .member: @@ -96,12 +111,24 @@ private enum CreateGroupEntry: ItemListNodeEntry { return 0 case .setProfilePhoto: return 1 - case .autoDelete: + case .usernameHeader: return 2 - case .autoDeleteInfo: + case .username: return 3 + case .usernameStatus: + return 4 + case .usernameInfo: + return 5 + case .topics: + return 6 + case .topicsInfo: + return 7 + case .autoDelete: + return 8 + case .autoDeleteInfo: + return 9 case let .member(index, _, _, _, _, _, _): - return 4 + index + return 10 + index case .locationHeader: return 10000 case .location: @@ -153,6 +180,43 @@ private enum CreateGroupEntry: ItemListNodeEntry { } else { return false } + + case let .usernameHeader(lhsTheme, lhsText): + if case let .usernameHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .username(lhsTheme, lhsText, lhsValue): + if case let .username(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .usernameStatus(lhsTheme, lhsAddressName, lhsStatus, lhsText, lhsUsername): + if case let .usernameStatus(rhsTheme, rhsAddressName, rhsStatus, rhsText, rhsUsername) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsStatus == rhsStatus, lhsText == rhsText, lhsUsername == rhsUsername { + return true + } else { + return false + } + case let .usernameInfo(lhsTheme, lhsText): + if case let .usernameInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .topics(lhsTheme, lhsText): + if case let .topics(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .topicsInfo(lhsTheme, lhsText): + if case let .topicsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .autoDelete(title, value): if case .autoDelete(title, value) = rhs { return true @@ -263,6 +327,41 @@ private enum CreateGroupEntry: ItemListNodeEntry { return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) + case let .usernameHeader(_, title): + return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) + case let .username(theme, placeholder, text): + return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in + arguments.updatePublicLinkText(updatedText) + }, action: { + }) + case let .usernameStatus(_, _, status, text, username): + var displayActivity = false + let textColor: ItemListActivityTextItem.TextColor + switch status { + case .invalidFormat: + textColor = .destructive + case let .availability(availability): + switch availability { + case .available: + textColor = .constructive + case .purchaseAvailable: + textColor = .generic + case .invalid, .taken: + textColor = .destructive + } + case .checking: + textColor = .generic + displayActivity = true + } + return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: text, color: textColor, linkAction: { _ in + arguments.openAuction(username) + }, sectionId: self.section) + case let .usernameInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) + case let .topics(_, text): + return ItemListSwitchItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Topics")?.precomposed(), title: text, value: true, enabled: false, sectionId: self.section, style: .blocks, updated: { _ in }) + case let .topicsInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .autoDelete(text, value): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { arguments.updateAutoDelete() @@ -299,9 +398,11 @@ private struct CreateGroupState: Equatable { var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? var location: PeerGeoLocation? var autoremoveTimeout: Int32? + var editingPublicLinkText: String? + var addressNameValidationStatus: AddressNameValidationStatus? } -private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView, venues: [TelegramMediaMap]?, globalAutoremoveTimeout: Int32) -> [CreateGroupEntry] { +private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView, venues: [TelegramMediaMap]?, globalAutoremoveTimeout: Int32, requestPeer: ReplyMarkupButtonRequestPeerType.Group?) -> [CreateGroupEntry] { var entries: [CreateGroupEntry] = [] let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) @@ -310,15 +411,68 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat entries.append(.groupInfo(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, groupInfoState, state.avatar)) - let autoremoveTimeout = state.autoremoveTimeout ?? globalAutoremoveTimeout - let autoRemoveText: String - if autoremoveTimeout == 0 { - autoRemoveText = presentationData.strings.Autoremove_OptionOff + if let requestPeer { + if let hasUsername = requestPeer.hasUsername, hasUsername { + let currentUsername = state.editingPublicLinkText ?? "" + entries.append(.usernameHeader(presentationData.theme, presentationData.strings.CreateGroup_PublicLinkTitle.uppercased())) + entries.append(.username(presentationData.theme, "link", currentUsername)) + + if let status = state.addressNameValidationStatus { + let statusText: String + switch status { + case let .invalidFormat(error): + switch error { + case .startsWithDigit: + statusText = presentationData.strings.Username_InvalidStartsWithNumber + case .startsWithUnderscore: + statusText = presentationData.strings.Username_InvalidStartsWithUnderscore + case .endsWithUnderscore: + statusText = presentationData.strings.Username_InvalidEndsWithUnderscore + case .invalidCharacters: + statusText = presentationData.strings.Username_InvalidCharacters + case .tooShort: + statusText = presentationData.strings.Username_InvalidTooShort + } + case let .availability(availability): + switch availability { + case .available: + statusText = presentationData.strings.Username_UsernameIsAvailable(currentUsername).string + case .invalid: + statusText = presentationData.strings.Username_InvalidCharacters + case .taken: + statusText = presentationData.strings.Username_InvalidTaken + case .purchaseAvailable: + var markdownString = presentationData.strings.Username_UsernamePurchaseAvailable + let entities = generateTextEntities(markdownString, enabledTypes: [.mention]) + if let entity = entities.first { + markdownString.insert(contentsOf: "]()", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.upperBound)) + markdownString.insert(contentsOf: "[", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.lowerBound)) + } + statusText = markdownString + } + case .checking: + statusText = presentationData.strings.Username_CheckingUsername + } + entries.append(.usernameStatus(presentationData.theme, currentUsername, status, statusText, currentUsername)) + } + + entries.append(.usernameInfo(presentationData.theme, presentationData.strings.CreateGroup_PublicLinkInfo)) + } + if let isForum = requestPeer.isForum, isForum { + entries.append(.topics(presentationData.theme, presentationData.strings.PeerInfo_OptionTopics)) + entries.append(.topicsInfo(presentationData.theme, presentationData.strings.PeerInfo_OptionTopicsText)) + } } else { - autoRemoveText = timeIntervalString(strings: presentationData.strings, value: autoremoveTimeout) + let autoremoveTimeout = state.autoremoveTimeout ?? globalAutoremoveTimeout + let autoRemoveText: String + if autoremoveTimeout == 0 { + autoRemoveText = presentationData.strings.Autoremove_OptionOff + } else { + autoRemoveText = timeIntervalString(strings: presentationData.strings, value: autoremoveTimeout) + } + entries.append(.autoDelete(title: presentationData.strings.CreateGroup_AutoDeleteTitle, value: autoRemoveText)) + entries.append(.autoDeleteInfo(presentationData.strings.CreateGroup_AutoDeleteText)) } - entries.append(.autoDelete(title: presentationData.strings.CreateGroup_AutoDeleteTitle, value: autoRemoveText)) - entries.append(.autoDeleteInfo(presentationData.strings.CreateGroup_AutoDeleteText)) var peers: [Peer] = [] for peerId in peerIds { @@ -382,7 +536,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] location = PeerGeoLocation(latitude: latitude, longitude: longitude, address: address ?? "") } - let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), nameSetFromVenue: false, avatar: nil, location: location, autoremoveTimeout: nil) + let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), nameSetFromVenue: false, avatar: nil, location: location, autoremoveTimeout: nil, editingPublicLinkText: nil, addressNameValidationStatus: nil) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in @@ -400,6 +554,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let actionsDisposable = DisposableSet() + let checkAddressNameDisposable = MetaDisposable() + actionsDisposable.add(checkAddressNameDisposable) + let currentAvatarMixin = Atomic(value: nil) let uploadedAvatar = Promise() @@ -429,13 +586,13 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] return current } }, done: { - let (creating, title, location) = stateValue.with { state -> (Bool, String, PeerGeoLocation?) in - return (state.creating, state.editingName.composedTitle, state.location) + let (creating, title, location, publicLink) = stateValue.with { state -> (Bool, String, PeerGeoLocation?, String?) in + return (state.creating, state.editingName.composedTitle, state.location, state.editingPublicLinkText) } if !creating && !title.isEmpty { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.GlobalAutoremoveTimeout()) - |> deliverOnMainQueue).start(next: { maybeGlobalAutoremoveTimeout in + |> deliverOnMainQueue).start(next: { maybeGlobalAutoremoveTimeout in updateState { current in var current = current current.creating = true @@ -447,7 +604,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let autoremoveTimeout = stateValue.with({ $0 }).autoremoveTimeout ?? globalAutoremoveTimeout let ttlPeriod: Int32? = autoremoveTimeout == 0 ? nil : autoremoveTimeout - let createSignal: Signal + var createSignal: Signal switch mode { case .generic: createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds, ttlPeriod: ttlPeriod) @@ -496,10 +653,85 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } } } + case let .requestPeer(group): + var isForum = false + if let isForumRequested = group.isForum, isForumRequested { + isForum = true + } + + if isForum { + createSignal = context.engine.peers.createSupergroup(title: title, description: nil, isForum: true) + |> map(Optional.init) + |> mapError { error -> CreateGroupError in + switch error { + case .generic: + return .generic + case .restricted: + return .restricted + case .tooMuchJoined: + return .tooMuchJoined + case .tooMuchLocationBasedGroups: + return .tooMuchLocationBasedGroups + case let .serverProvided(error): + return .serverProvided(error) + } + } + + if let publicLink, !publicLink.isEmpty { + createSignal = createSignal + |> mapToSignal { peerId in + if let peerId = peerId { + return context.engine.peers.updateAddressName(domain: .peer(peerId), name: publicLink) + |> mapError { _ in + return .generic + } + |> map { _ in + return peerId + } + } else { + return .fail(.generic) + } + } + } + } else { + if let publicLink, !publicLink.isEmpty { + createSignal = context.engine.peers.createSupergroup(title: title, description: nil) + |> map(Optional.init) + |> mapError { error -> CreateGroupError in + switch error { + case .generic: + return .generic + case .restricted: + return .restricted + case .tooMuchJoined: + return .tooMuchJoined + case .tooMuchLocationBasedGroups: + return .tooMuchLocationBasedGroups + case let .serverProvided(error): + return .serverProvided(error) + } + } + |> mapToSignal { peerId in + if let peerId = peerId { + return context.engine.peers.updateAddressName(domain: .peer(peerId), name: publicLink) + |> mapError { _ in + return .generic + } + |> map { _ in + return peerId + } + } else { + return .fail(.generic) + } + } + } else { + createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds, ttlPeriod: nil) + } + } } actionsDisposable.add((createSignal - |> mapToSignal { peerId -> Signal in + |> mapToSignal { peerId -> Signal in guard let peerId = peerId else { return .single(nil) } @@ -910,8 +1142,42 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] presentInGlobalOverlay?(contextController) } }) + }, updatePublicLinkText: { text in + if text.isEmpty { + checkAddressNameDisposable.set(nil) + updateState { state in + var updated = state + updated.editingPublicLinkText = text + updated.addressNameValidationStatus = nil + return updated + } + } else { + updateState { state in + var updated = state + updated.editingPublicLinkText = text + return updated + } + + checkAddressNameDisposable.set((context.engine.peers.validateAddressNameInteractive(domain: .peer(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(0))), name: text) + |> deliverOnMainQueue).start(next: { result in + updateState { state in + var updated = state + updated.addressNameValidationStatus = result + return updated + } + })) + } + }, openAuction: { username in + endEditingImpl?() + + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/username/\(username)", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }) + var requestPeer: ReplyMarkupButtonRequestPeerType.Group? + if case let .requestPeer(peerType) = mode { + requestPeer = peerType + } + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), @@ -921,7 +1187,6 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.GlobalAutoremoveTimeout()) ) |> map { presentationData, state, view, address, venues, globalAutoremoveTimeout -> (ItemListControllerState, (ItemListNodeState, Any)) in - let rightNavigationButton: ItemListNavigationButton if state.creating { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) @@ -932,7 +1197,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Compose_NewGroupTitle), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view, venues: venues, globalAutoremoveTimeout: globalAutoremoveTimeout ?? 0), style: .blocks, focusItemTag: CreateGroupEntryTag.info) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view, venues: venues, globalAutoremoveTimeout: globalAutoremoveTimeout ?? 0, requestPeer: requestPeer), style: .blocks, focusItemTag: CreateGroupEntryTag.info) return (controllerState, (listState, arguments)) } @@ -941,6 +1206,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } let controller = ItemListController(context: context, state: signal) + controller.beganInteractiveDragging = { + endEditingImpl?() + } replaceControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 4c7ef91f57..662de831d5 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -158,6 +158,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _ in + }, openRequestedPeerSelection: { _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 2f25240593..7160576db5 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2728,6 +2728,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _ in + }, openRequestedPeerSelection: { _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { @@ -3093,8 +3094,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate return } } - - if peer.firstName != firstName || peer.lastName != lastName || (bio != nil && bio != cachedData.about) { + + if (peer.firstName ?? "") != firstName || (peer.lastName ?? "") != lastName || (bio ?? "") != (cachedData.about ?? "") { var updateNameSignal: Signal = .complete() var hasProgress = false if peer.firstName != firstName || peer.lastName != lastName { diff --git a/submodules/TelegramUI/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Sources/PeerSelectionController.swift index e2bdace938..d8767a7419 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionController.swift @@ -62,6 +62,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon private let pretendPresentedInModal: Bool private let forwardedMessageIds: [EngineMessage.Id] private let hasTypeHeaders: Bool + private let requestPeerType: ReplyMarkupButtonRequestPeerType? override public var _presentedInModal: Bool { get { @@ -93,12 +94,29 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self.forwardedMessageIds = params.forwardedMessageIds self.hasTypeHeaders = params.hasTypeHeaders self.selectForumThreads = params.selectForumThreads + self.requestPeerType = params.requestPeerType super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.customTitle = params.title + + if let peerType = params.requestPeerType { + switch peerType { + case let .user(user): + if let isBot = user.isBot, isBot { + self.customTitle = self.presentationData.strings.RequestPeer_ChooseBotTitle + } else { + self.customTitle = self.presentationData.strings.RequestPeer_ChooseUserTitle + } + case .group: + self.customTitle = self.presentationData.strings.RequestPeer_ChooseGroupTitle + case .channel: + self.customTitle = self.presentationData.strings.RequestPeer_ChooseChannelTitle + } + } + self.title = self.customTitle ?? self.presentationData.strings.Conversation_ForwardTitle if params.forumPeerId == nil { @@ -159,7 +177,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon } override public func loadDisplayNode() { - self.displayNode = PeerSelectionControllerNode(context: self.context, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, createNewGroup: self.createNewGroup, present: { [weak self] c, a in + self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, createNewGroup: self.createNewGroup, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) }, presentInGlobalOverlay: { [weak self] c, a in self?.presentInGlobalOverlay(c, with: a) diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index c29cfded5b..f9b9923ef6 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -18,9 +18,13 @@ import ChatSendMessageActionUI import ChatTextLinkEditUI import AnimationCache import MultiAnimationRenderer +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import SolidRoundedButtonNode final class PeerSelectionControllerNode: ASDisplayNode { private let context: AccountContext + private weak var controller: PeerSelectionController? private let present: (ViewController, Any?) -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void private let dismiss: () -> Void @@ -29,6 +33,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { private let hasGlobalSearch: Bool private let forwardedMessageIds: [EngineMessage.Id] private let hasTypeHeaders: Bool + private let requestPeerType: ReplyMarkupButtonRequestPeerType? private var presentationInterfaceState: ChatPresentationInterfaceState private var interfaceInteraction: ChatPanelInterfaceInteraction? @@ -41,6 +46,16 @@ final class PeerSelectionControllerNode: ASDisplayNode { var navigationBar: NavigationBar? + private let requirementsBackgroundNode: NavigationBackgroundNode? + private let requirementsSeparatorNode: ASDisplayNode? + private let requirementsTextNode: ImmediateTextNode? + + private let emptyAnimationNode: AnimatedStickerNode + private var emptyAnimationSize = CGSize() + private let emptyTitleNode: ImmediateTextNode + private let emptyTextNode: ImmediateTextNode + private let emptyButtonNode: SolidRoundedButtonNode + private let toolbarBackgroundNode: NavigationBackgroundNode? private let toolbarSeparatorNode: ASDisplayNode? private let segmentedControlNode: SegmentedControlNode? @@ -83,12 +98,15 @@ final class PeerSelectionControllerNode: ASDisplayNode { return self.readyValue.get() } + private var isEmpty = false + private var updatedPresentationData: (initial: PresentationData, signal: Signal) { return (self.presentationData, self.presentationDataPromise.get()) } - init(context: AccountContext, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { + init(context: AccountContext, controller: PeerSelectionController, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: ReplyMarkupButtonRequestPeerType?, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) { self.context = context + self.controller = controller self.present = present self.presentInGlobalOverlay = presentInGlobalOverlay self.dismiss = dismiss @@ -97,6 +115,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.hasGlobalSearch = hasGlobalSearch self.forwardedMessageIds = forwardedMessageIds self.hasTypeHeaders = hasTypeHeaders + self.requestPeerType = requestPeerType self.presentationData = presentationData @@ -107,6 +126,43 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } + if let _ = self.requestPeerType { + self.requirementsBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) + self.requirementsSeparatorNode = ASDisplayNode() + self.requirementsSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor + self.requirementsTextNode = ImmediateTextNode() + self.requirementsTextNode?.maximumNumberOfLines = 0 + self.requirementsTextNode?.lineSpacing = 0.1 + } else { + self.requirementsBackgroundNode = nil + self.requirementsSeparatorNode = nil + self.requirementsTextNode = nil + } + + self.emptyTitleNode = ImmediateTextNode() + self.emptyTitleNode.displaysAsynchronously = false + self.emptyTitleNode.maximumNumberOfLines = 0 + self.emptyTitleNode.isHidden = true + self.emptyTitleNode.textAlignment = .center + self.emptyTitleNode.lineSpacing = 0.25 + + self.emptyTextNode = ImmediateTextNode() + self.emptyTextNode.displaysAsynchronously = false + self.emptyTextNode.maximumNumberOfLines = 0 + self.emptyTextNode.isHidden = true + self.emptyTextNode.lineSpacing = 0.25 + + self.emptyAnimationNode = DefaultAnimatedStickerNodeImpl() + self.emptyAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListNoResults"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + self.emptyAnimationNode.isHidden = true + self.emptyAnimationSize = CGSize(width: 120.0, height: 120.0) + + self.emptyButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), cornerRadius: 11.0, gloss: true) + self.emptyButtonNode.isHidden = true + self.emptyButtonNode.pressed = { + createNewGroup?() + } + if hasChatListSelector && hasContactSelector { self.toolbarBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) @@ -136,8 +192,15 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { chatListLocation = .chatList(groupId: .root) } + + let chatListMode: ChatListNodeMode + if let requestPeerType = self.requestPeerType { + chatListMode = .peerType(type: requestPeerType) + } else { + chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false) + } - self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false) + self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false) super.init() @@ -184,6 +247,17 @@ final class PeerSelectionControllerNode: ASDisplayNode { return self?.contentScrollingEnded?(listView) ?? false } + self.chatListNode.isEmptyUpdated = { [weak self] state, _, _ in + guard let strongSelf = self else { + return + } + if case .empty(false, _) = state, let (layout, navigationBarHeight, actualNavigationBarHeight) = strongSelf.containerLayout { + strongSelf.isEmpty = true + strongSelf.controller?.navigationBar?.setContentNode(nil, animated: false) + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate) + } + } + self.addSubnode(self.chatListNode) if hasChatListSelector && hasContactSelector { @@ -196,6 +270,17 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.addSubnode(self.segmentedControlNode!) } + if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode { + self.chatListNode.addSubnode(requirementsBackgroundNode) + self.chatListNode.addSubnode(requirementsSeparatorNode) + self.chatListNode.addSubnode(requirementsTextNode) + + self.addSubnode(self.emptyAnimationNode) + self.addSubnode(self.emptyTitleNode) + self.addSubnode(self.emptyTextNode) + self.addSubnode(self.emptyButtonNode) + } + if !hasChatListSelector && hasContactSelector { self.indexChanged(1) } @@ -484,6 +569,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.updateChatPresentationInterfaceState({ $0.updatedTheme(self.presentationData.theme) }) + self.requirementsBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.toolbarBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.segmentedControlNode?.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme)) @@ -574,6 +660,113 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + if let requestPeerType = self.requestPeerType { + if self.isEmpty { + self.chatListNode.isHidden = true + self.requirementsBackgroundNode?.isHidden = true + self.requirementsTextNode?.isHidden = true + self.requirementsSeparatorNode?.isHidden = true + self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + + var emptyTitle: String + var emptyText: String + var emptyButtonText: String + switch requestPeerType { + case let .user(user): + if let isBot = user.isBot, isBot { + emptyTitle = self.presentationData.strings.RequestPeer_BotsAllEmpty + emptyText = "" + } else { + emptyTitle = self.presentationData.strings.RequestPeer_UsersAllEmpty + if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) { + emptyTitle = self.presentationData.strings.RequestPeer_UsersEmpty + emptyText = text + } else { + emptyText = "" + } + } + emptyButtonText = "" + case .group: + emptyTitle = self.presentationData.strings.RequestPeer_GroupsAllEmpty + if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) { + emptyTitle = self.presentationData.strings.RequestPeer_GroupsEmpty + emptyText = text + } else { + emptyText = "" + } + emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup + case .channel: + emptyTitle = self.presentationData.strings.RequestPeer_ChannelsEmpty + if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) { + emptyTitle = self.presentationData.strings.RequestPeer_ChannelsEmpty + emptyText = text + } else { + emptyText = "" + } + emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup + } + + self.emptyTitleNode.attributedText = NSAttributedString(string: emptyTitle, font: Font.semibold(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: Font.regular(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + + let padding: CGFloat = 44.0 + let emptyTitleSize = self.emptyTitleNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + let emptyAnimationHeight = self.emptyAnimationSize.height + let emptyAnimationSpacing: CGFloat = 12.0 + let emptyTextSpacing: CGFloat = 17.0 + var emptyButtonSpacing: CGFloat = 15.0 + var emptyButtonHeight: CGFloat = 50.0 + if emptyButtonText.isEmpty { + emptyButtonSpacing = 0.0 + emptyButtonHeight = 0.0 + } + let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + emptyButtonSpacing + emptyButtonHeight + let emptyAnimationY = floorToScreenPixels((layout.size.height - emptyTotalHeight) / 2.0) + + if !emptyButtonText.isEmpty { + let buttonPadding: CGFloat = 30.0 + self.emptyButtonNode.title = emptyButtonText + self.emptyButtonNode.isHidden = false + let emptyButtonWidth = layout.size.width - insets.left - insets.right - buttonPadding * 2.0 + let _ = self.emptyButtonNode.updateLayout(width: emptyButtonWidth, transition: transition) + transition.updateFrame(node: self.emptyButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyButtonWidth) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing + emptyTextSize.height + emptyButtonSpacing), size: CGSize(width: emptyButtonWidth, height: emptyButtonHeight))) + } else { + self.emptyButtonNode.isHidden = true + } + + let textTransition = ContainedViewLayoutTransition.immediate + textTransition.updateFrame(node: self.emptyAnimationNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - self.emptyAnimationSize.width) / 2.0), y: emptyAnimationY), size: self.emptyAnimationSize)) + textTransition.updateFrame(node: self.emptyTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTitleSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) + textTransition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTextSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) + self.emptyAnimationNode.updateLayout(size: self.emptyAnimationSize) + + self.emptyAnimationNode.isHidden = false + self.emptyTitleNode.isHidden = false + self.emptyTextNode.isHidden = false + self.emptyAnimationNode.visibility = true + } else if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode, let requirementsText = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: true) { + let requirements = NSMutableAttributedString(string: self.presentationData.strings.RequestPeer_Requirements + "\n", font: Font.semibold(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) + requirements.append(NSAttributedString(string: requirementsText, font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)) + + requirementsTextNode.attributedText = requirements + let sideInset: CGFloat = 16.0 + let verticalInset: CGFloat = 11.0 + let requirementsSize = requirementsTextNode.updateLayout(CGSize(width: layout.size.width - insets.left - insets.right - sideInset * 2.0, height: .greatestFiniteMagnitude)) + + let requirementsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: actualNavigationBarHeight), size: CGSize(width: layout.size.width, height: requirementsSize.height + verticalInset * 2.0)) + insets.top += requirementsBackgroundFrame.height + + requirementsBackgroundNode.update(size: requirementsBackgroundFrame.size, transition: transition) + transition.updateFrame(node: requirementsBackgroundNode, frame: requirementsBackgroundFrame) + + transition.updateFrame(node: requirementsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: requirementsBackgroundFrame.maxY - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + requirementsTextNode.frame = CGRect(origin: CGPoint(x: insets.left + sideInset, y: requirementsBackgroundFrame.minY + verticalInset), size: requirementsSize) + } + } + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) @@ -614,6 +807,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { animationRenderer: self.animationRenderer, updatedPresentationData: self.updatedPresentationData, filter: self.filter, + requestPeerType: self.requestPeerType, location: chatListLocation, displaySearchFilters: false, hasDownloads: false, @@ -865,3 +1059,100 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } } + +private func stringForRequestPeerType(strings: PresentationStrings, peerType: ReplyMarkupButtonRequestPeerType, offset: Bool) -> String? { + var lines: [String] = [] + + func append(_ string: String) { + if offset { + lines.append(" • \(string)") + } else { + lines.append("• \(string)") + } + } + + switch peerType { + case let .user(user): + if let isPremium = user.isPremium { + if isPremium { + append(strings.RequestPeer_Requirement_UserPremiumOn) + } else { + append(strings.RequestPeer_Requirement_UserPremiumOff) + } + } + case let .group(group): + if group.isCreator { + append(strings.RequestPeer_Requirement_Group_CreatorOn) + } + if let hasUsername = group.hasUsername { + if hasUsername { + append(strings.RequestPeer_Requirement_Group_HasUsernameOn) + } else { + append(strings.RequestPeer_Requirement_Group_HasUsernameOff) + } + } + if let isForum = group.isForum { + if isForum { + append(strings.RequestPeer_Requirement_Group_ForumOn) + } else { + append(strings.RequestPeer_Requirement_Group_ForumOff) + } + } + if let adminRights = group.userAdminRights { + var rights: [String] = [] + if adminRights.rights.contains(.canChangeInfo) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_Info) + } + if adminRights.rights.contains(.canPostMessages) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_Send) + } + if adminRights.rights.contains(.canDeleteMessages) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_Delete) + } + if adminRights.rights.contains(.canEditMessages) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_Edit) + } + if adminRights.rights.contains(.canBanUsers) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_Ban) + } + if adminRights.rights.contains(.canInviteUsers) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_Invite) + } + if adminRights.rights.contains(.canPinMessages) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_Pin) + } + if adminRights.rights.contains(.canManageTopics) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_Topics) + } + if adminRights.rights.contains(.canManageCalls) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_VideoChats) + } + if adminRights.rights.contains(.canBeAnonymous) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_Anonymous) + } + if adminRights.rights.contains(.canAddAdmins) { + rights.append(strings.RequestPeer_Requirement_Group_Rights_AddAdmins) + } + if !rights.isEmpty { + let rightsString = strings.RequestPeer_Requirement_Group_Rights(String(rights.joined(separator: ", "))).string + append(rightsString) + } + } + case let .channel(channel): + if channel.isCreator { + append(strings.RequestPeer_Requirement_Channel_CreatorOn) + } + if let hasUsername = channel.hasUsername { + if hasUsername { + append(strings.RequestPeer_Requirement_Channel_HasUsernameOn) + } else { + append(strings.RequestPeer_Requirement_Channel_HasUsernameOff) + } + } + } + if lines.isEmpty { + return nil + } else { + return String(lines.joined(separator: "\n")) + } +} diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 7d16377cad..a5e396a520 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1379,6 +1379,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in }, activateAdAction: { _ in + }, openRequestedPeerSelection: { _, _, _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 761909501a..7e61c20f76 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -41,7 +41,6 @@ public struct ExperimentalUISettings: Codable, Equatable { public var enableDebugDataDisplay: Bool public var acceleratedStickers: Bool public var experimentalBackground: Bool - public var snow: Bool public var inlineStickers: Bool public var localTranscription: Bool public var enableReactionOverrides: Bool @@ -49,6 +48,9 @@ public struct ExperimentalUISettings: Codable, Equatable { public var accountReactionEffectOverrides: [AccountReactionOverrides] public var accountStickerEffectOverrides: [AccountReactionOverrides] public var disableQuickReaction: Bool + public var disableLanguageRecognition: Bool + public var disableImageContentAnalysis: Bool + public var disableBackgroundAnimation: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -67,14 +69,16 @@ public struct ExperimentalUISettings: Codable, Equatable { enableDebugDataDisplay: false, acceleratedStickers: false, experimentalBackground: false, - snow: false, inlineStickers: false, localTranscription: false, enableReactionOverrides: false, inlineForums: false, accountReactionEffectOverrides: [], accountStickerEffectOverrides: [], - disableQuickReaction: false + disableQuickReaction: false, + disableLanguageRecognition: false, + disableImageContentAnalysis: false, + disableBackgroundAnimation: false ) } @@ -94,14 +98,16 @@ public struct ExperimentalUISettings: Codable, Equatable { enableDebugDataDisplay: Bool, acceleratedStickers: Bool, experimentalBackground: Bool, - snow: Bool, inlineStickers: Bool, localTranscription: Bool, enableReactionOverrides: Bool, inlineForums: Bool, accountReactionEffectOverrides: [AccountReactionOverrides], accountStickerEffectOverrides: [AccountReactionOverrides], - disableQuickReaction: Bool + disableQuickReaction: Bool, + disableLanguageRecognition: Bool, + disableImageContentAnalysis: Bool, + disableBackgroundAnimation: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -118,7 +124,6 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableDebugDataDisplay = enableDebugDataDisplay self.acceleratedStickers = acceleratedStickers self.experimentalBackground = experimentalBackground - self.snow = snow self.inlineStickers = inlineStickers self.localTranscription = localTranscription self.enableReactionOverrides = enableReactionOverrides @@ -126,6 +131,9 @@ public struct ExperimentalUISettings: Codable, Equatable { self.accountReactionEffectOverrides = accountReactionEffectOverrides self.accountStickerEffectOverrides = accountStickerEffectOverrides self.disableQuickReaction = disableQuickReaction + self.disableLanguageRecognition = disableLanguageRecognition + self.disableImageContentAnalysis = disableImageContentAnalysis + self.disableBackgroundAnimation = disableBackgroundAnimation } public init(from decoder: Decoder) throws { @@ -146,7 +154,6 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0 self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 0) != 0 self.experimentalBackground = (try container.decodeIfPresent(Int32.self, forKey: "experimentalBackground") ?? 0) != 0 - self.snow = (try container.decodeIfPresent(Int32.self, forKey: "snow") ?? 0) != 0 self.inlineStickers = (try container.decodeIfPresent(Int32.self, forKey: "inlineStickers") ?? 0) != 0 self.localTranscription = (try container.decodeIfPresent(Int32.self, forKey: "localTranscription") ?? 0) != 0 self.enableReactionOverrides = try container.decodeIfPresent(Bool.self, forKey: "enableReactionOverrides") ?? false @@ -154,6 +161,9 @@ public struct ExperimentalUISettings: Codable, Equatable { self.accountReactionEffectOverrides = (try? container.decodeIfPresent([AccountReactionOverrides].self, forKey: "accountReactionEffectOverrides")) ?? [] self.accountStickerEffectOverrides = (try? container.decodeIfPresent([AccountReactionOverrides].self, forKey: "accountStickerEffectOverrides")) ?? [] self.disableQuickReaction = try container.decodeIfPresent(Bool.self, forKey: "disableQuickReaction") ?? false + self.disableLanguageRecognition = try container.decodeIfPresent(Bool.self, forKey: "disableLanguageRecognition") ?? false + self.disableImageContentAnalysis = try container.decodeIfPresent(Bool.self, forKey: "disableImageContentAnalysis") ?? false + self.disableBackgroundAnimation = try container.decodeIfPresent(Bool.self, forKey: "disableBackgroundAnimation") ?? false } public func encode(to encoder: Encoder) throws { @@ -174,7 +184,6 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay") try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers") try container.encode((self.experimentalBackground ? 1 : 0) as Int32, forKey: "experimentalBackground") - try container.encode((self.snow ? 1 : 0) as Int32, forKey: "snow") try container.encode((self.inlineStickers ? 1 : 0) as Int32, forKey: "inlineStickers") try container.encode((self.localTranscription ? 1 : 0) as Int32, forKey: "localTranscription") try container.encode(self.enableReactionOverrides, forKey: "enableReactionOverrides") @@ -182,6 +191,9 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode(self.accountReactionEffectOverrides, forKey: "accountReactionEffectOverrides") try container.encode(self.accountStickerEffectOverrides, forKey: "accountStickerEffectOverrides") try container.encode(self.disableQuickReaction, forKey: "disableQuickReaction") + try container.encode(self.disableLanguageRecognition, forKey: "disableLanguageRecognition") + try container.encode(self.disableImageContentAnalysis, forKey: "disableImageContentAnalysis") + try container.encode(self.disableBackgroundAnimation, forKey: "disableBackgroundAnimation") } } diff --git a/submodules/TranslateUI/Sources/Translate.swift b/submodules/TranslateUI/Sources/Translate.swift index 6ccec4617f..b7ba2f43e8 100644 --- a/submodules/TranslateUI/Sources/Translate.swift +++ b/submodules/TranslateUI/Sources/Translate.swift @@ -147,6 +147,10 @@ public func canTranslateText(context: AccountContext, text: String, showTranslat } if #available(iOS 12.0, *) { + if context.sharedContext.immediateExperimentalUISettings.disableLanguageRecognition { + return (true, nil) + } + var dontTranslateLanguages: [String] = [] if let ignoredLanguages = ignoredLanguages { dontTranslateLanguages = ignoredLanguages