Bot peer request support

Launch screen icon
This commit is contained in:
Ilya Laktyushin 2023-01-13 22:38:04 +04:00
parent 58e641230a
commit 930e237bf1
37 changed files with 1242 additions and 125 deletions

View File

@ -1,19 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" interfaceStyle="light" id="O8c-13-3vw"> <view contentMode="scaleToFill" id="O8c-13-3vw">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Chat/Links/QrLogo" translatesAutoresizingMaskIntoConstraints="NO" id="Ra6-Gz-QsF">
<rect key="frame" x="147" y="388" width="120" height="120"/>
<constraints>
<constraint firstAttribute="width" constant="120" id="Mhj-0F-KYG"/>
<constraint firstAttribute="height" constant="120" id="gdP-J3-bQE"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="Ra6-Gz-QsF" firstAttribute="centerY" secondItem="O8c-13-3vw" secondAttribute="centerY" id="54l-Rw-Siu"/>
<constraint firstItem="Ra6-Gz-QsF" firstAttribute="centerX" secondItem="O8c-13-3vw" secondAttribute="centerX" id="zdX-Zd-oe6"/>
</constraints>
<point key="canvasLocation" x="139" y="117"/> <point key="canvasLocation" x="139" y="117"/>
</view> </view>
</objects> </objects>
<resources>
<image name="Chat/Links/QrLogo" width="72" height="72"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document> </document>

View File

@ -8624,3 +8624,69 @@ Sorry for the inconvenience.";
"StorageManagement.OpenFile" = "Open File"; "StorageManagement.OpenFile" = "Open File";
"ChatListFilter.AddChatsSearchPlaceholder" = "Search Chats"; "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";

View File

@ -719,6 +719,7 @@ public enum CreateGroupMode {
case generic case generic
case supergroup case supergroup
case locatedGroup(latitude: Double, longitude: Double, address: String?) case locatedGroup(latitude: Double, longitude: Double, address: String?)
case requestPeer(ReplyMarkupButtonRequestPeerType.Group)
} }
public protocol AppLockContext: AnyObject { public protocol AppLockContext: AnyObject {

View File

@ -41,6 +41,7 @@ public final class PeerSelectionControllerParams {
public let context: AccountContext public let context: AccountContext
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
public let filter: ChatListNodePeersFilter public let filter: ChatListNodePeersFilter
public let requestPeerType: ReplyMarkupButtonRequestPeerType?
public let forumPeerId: EnginePeer.Id? public let forumPeerId: EnginePeer.Id?
public let hasChatListSelector: Bool public let hasChatListSelector: Bool
public let hasContactSelector: Bool public let hasContactSelector: Bool
@ -54,10 +55,11 @@ public final class PeerSelectionControllerParams {
public let hasTypeHeaders: Bool public let hasTypeHeaders: Bool
public let selectForumThreads: Bool public let selectForumThreads: Bool
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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.context = context
self.updatedPresentationData = updatedPresentationData self.updatedPresentationData = updatedPresentationData
self.filter = filter self.filter = filter
self.requestPeerType = requestPeerType
self.forumPeerId = forumPeerId self.forumPeerId = forumPeerId
self.hasChatListSelector = hasChatListSelector self.hasChatListSelector = hasChatListSelector
self.hasContactSelector = hasContactSelector self.hasContactSelector = hasContactSelector

View File

@ -1117,8 +1117,11 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
let wasEmpty = self.viewControllers.isEmpty let wasEmpty = self.viewControllers.isEmpty
super.setViewControllers(viewControllers, animated: animated) super.setViewControllers(viewControllers, animated: animated)
if wasEmpty { if wasEmpty {
if self.topViewController is AuthorizationSequenceSplashController {
} else {
self.topViewController?.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.topViewController?.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
} }
}
if !self.didSetReady { if !self.didSetReady {
self.didSetReady = true self.didSetReady = true
self._ready.set(.single(true)) self._ready.set(.single(true))
@ -1150,7 +1153,13 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
} }
private func animateIn() { private func animateIn() {
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) 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) { private func animateOut(completion: (() -> Void)? = nil) {

View File

@ -10,7 +10,7 @@ import LegacyComponents
import SolidRoundedButtonNode import SolidRoundedButtonNode
import RMIntro import RMIntro
final class AuthorizationSequenceSplashController: ViewController { public final class AuthorizationSequenceSplashController: ViewController {
private var controllerNode: AuthorizationSequenceSplashControllerNode { private var controllerNode: AuthorizationSequenceSplashControllerNode {
return self.displayNode as! AuthorizationSequenceSplashControllerNode return self.displayNode as! AuthorizationSequenceSplashControllerNode
} }
@ -107,11 +107,15 @@ final class AuthorizationSequenceSplashController: ViewController {
self.activateLocalizationDisposable.dispose() self.activateLocalizationDisposable.dispose()
} }
override public func loadDisplayNode() { public override func loadDisplayNode() {
self.displayNode = AuthorizationSequenceSplashControllerNode(theme: self.theme) self.displayNode = AuthorizationSequenceSplashControllerNode(theme: self.theme)
self.displayNodeDidLoad() self.displayNodeDidLoad()
} }
func animateIn() {
self.controller.animateIn()
}
var buttonFrame: CGRect { var buttonFrame: CGRect {
return self.startButton.frame 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) super.viewWillAppear(animated)
self.addControllerIfNeeded() self.addControllerIfNeeded()
self.controller.viewWillAppear(false) self.controller.viewWillAppear(false)
} }
override func viewDidAppear(_ animated: Bool) { public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
controller.viewDidAppear(animated) controller.viewDidAppear(animated)
} }
override func viewWillDisappear(_ animated: Bool) { public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
controller.viewWillDisappear(animated) controller.viewWillDisappear(animated)
} }
override func viewDidDisappear(_ animated: Bool) { public override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
controller.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) super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout self.validLayout = layout

View File

@ -121,8 +121,10 @@ public class ChatListAdditionalCategoryItem: ItemListItem, ListViewItemWithHeade
} }
} else if let _ = nextItem as? ChatListAdditionalCategoryItem { } else if let _ = nextItem as? ChatListAdditionalCategoryItem {
} else { } else {
if let nextItem = nextItem as? ListViewItemWithHeader, nextItem.header != nil {
last = true last = true
} }
}
} else { } else {
last = true last = true
} }

View File

@ -1535,7 +1535,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
//filter.insert(.excludeRecent) //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) self?.requestOpenPeerFromSearch?(peer, threadId, dismissSearch)
}, openDisabledPeer: { _, _ in }, openDisabledPeer: { _, _ in
}, openRecentPeerOptions: { [weak self] peer in }, openRecentPeerOptions: { [weak self] peer in

View File

@ -86,6 +86,7 @@ private struct ChatListSearchContainerNodeSearchState: Equatable {
public final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { public final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
private let context: AccountContext private let context: AccountContext
private let peersFilter: ChatListNodePeersFilter private let peersFilter: ChatListNodePeersFilter
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private let location: ChatListControllerLocation private let location: ChatListControllerLocation
private let displaySearchFilters: Bool private let displaySearchFilters: Bool
private let hasDownloads: Bool private let hasDownloads: Bool
@ -135,7 +136,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private var validLayout: (ContainerViewLayout, CGFloat)? private var validLayout: (ContainerViewLayout, CGFloat)?
public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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 var initialFilter = initialFilter
if case .chats = initialFilter, case .forum = location { if case .chats = initialFilter, case .forum = location {
initialFilter = .topics initialFilter = .topics
@ -143,6 +144,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.context = context self.context = context
self.peersFilter = filter self.peersFilter = filter
self.requestPeerType = requestPeerType
self.location = location self.location = location
self.displaySearchFilters = displaySearchFilters self.displaySearchFilters = displaySearchFilters
self.hasDownloads = hasDownloads self.hasDownloads = hasDownloads
@ -160,7 +162,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
self.filterContainerNode = ChatListSearchFiltersContainerNode() 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 self.paneContainerNode.clipsToBounds = true
super.init() super.init()

View File

@ -933,6 +933,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let animationRenderer: MultiAnimationRenderer private let animationRenderer: MultiAnimationRenderer
private let interaction: ChatListSearchInteraction private let interaction: ChatListSearchInteraction
private let peersFilter: ChatListNodePeersFilter private let peersFilter: ChatListNodePeersFilter
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private var presentationData: PresentationData private var presentationData: PresentationData
private let key: ChatListSearchPaneKey private let key: ChatListSearchPaneKey
private let tagMask: EngineMessage.Tags? private let tagMask: EngineMessage.Tags?
@ -981,7 +982,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let emptyResultsTitleNode: ImmediateTextNode private let emptyResultsTitleNode: ImmediateTextNode
private let emptyResultsTextNode: ImmediateTextNode private let emptyResultsTextNode: ImmediateTextNode
private let emptyResultsAnimationNode: AnimatedStickerNode 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)? 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? private var hiddenMediaDisposable: Disposable?
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) { init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: ReplyMarkupButtonRequestPeerType?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
self.context = context self.context = context
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
@ -1018,6 +1019,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
peersFilter.insert(.excludeRecent) peersFilter.insert(.excludeRecent)
} }
self.peersFilter = peersFilter self.peersFilter = peersFilter
self.requestPeerType = requestPeerType
let tagMask: EngineMessage.Tags? let tagMask: EngineMessage.Tags?
switch key { switch key {
@ -1645,6 +1647,96 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let _ = currentRemotePeers.swap((foundRemotePeers.0, foundRemotePeers.1)) let _ = currentRemotePeers.swap((foundRemotePeers.0, foundRemotePeers.1))
let filteredPeer: (EnginePeer, EnginePeer) -> Bool = { peer, accountPeer in let filteredPeer: (EnginePeer, EnginePeer) -> Bool = { peer, accountPeer in
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
}
}
} else {
guard !peersFilter.contains(.excludeSavedMessages) || peer.id != accountPeer.id else { return false } 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(.excludeSecretChats) || peer.id.namespace != Namespaces.Peer.SecretChat else { return false }
guard !peersFilter.contains(.onlyPrivateChats) || peer.id.namespace == Namespaces.Peer.CloudUser else { return false } guard !peersFilter.contains(.onlyPrivateChats) || peer.id.namespace == Namespaces.Peer.CloudUser else { return false }
@ -1674,6 +1766,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
return false return false
} }
} }
}
return true return true
} }

View File

@ -119,13 +119,14 @@ private final class ChatListSearchPendingPane {
interaction: ChatListSearchInteraction, interaction: ChatListSearchInteraction,
navigationController: NavigationController?, navigationController: NavigationController?,
peersFilter: ChatListNodePeersFilter, peersFilter: ChatListNodePeersFilter,
requestPeerType: ReplyMarkupButtonRequestPeerType?,
location: ChatListControllerLocation, location: ChatListControllerLocation,
searchQuery: Signal<String?, NoError>, searchQuery: Signal<String?, NoError>,
searchOptions: Signal<ChatListSearchOptions?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>,
key: ChatListSearchPaneKey, key: ChatListSearchPaneKey,
hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void 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.pane = ChatListSearchPaneWrapper(key: key, node: paneNode)
self.disposable = (paneNode.isReady self.disposable = (paneNode.isReady
@ -147,6 +148,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
private let animationRenderer: MultiAnimationRenderer private let animationRenderer: MultiAnimationRenderer
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
private let peersFilter: ChatListNodePeersFilter private let peersFilter: ChatListNodePeersFilter
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private let location: ChatListControllerLocation private let location: ChatListControllerLocation
private let searchQuery: Signal<String?, NoError> private let searchQuery: Signal<String?, NoError>
private let searchOptions: Signal<ChatListSearchOptions?, NoError> private let searchOptions: Signal<ChatListSearchOptions?, NoError>
@ -181,12 +183,13 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
private var currentAvailablePanes: [ChatListSearchPaneKey]? private var currentAvailablePanes: [ChatListSearchPaneKey]?
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peersFilter: ChatListNodePeersFilter, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) { init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peersFilter: ChatListNodePeersFilter, requestPeerType: ReplyMarkupButtonRequestPeerType?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
self.context = context self.context = context
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
self.updatedPresentationData = updatedPresentationData self.updatedPresentationData = updatedPresentationData
self.peersFilter = peersFilter self.peersFilter = peersFilter
self.requestPeerType = requestPeerType
self.location = location self.location = location
self.searchQuery = searchQuery self.searchQuery = searchQuery
self.searchOptions = searchOptions self.searchOptions = searchOptions
@ -420,6 +423,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
interaction: self.interaction!, interaction: self.interaction!,
navigationController: self.navigationController, navigationController: self.navigationController,
peersFilter: self.peersFilter, peersFilter: self.peersFilter,
requestPeerType: self.requestPeerType,
location: self.location, location: self.location,
searchQuery: self.searchQuery, searchQuery: self.searchQuery,
searchOptions: self.searchOptions, searchOptions: self.searchOptions,

View File

@ -21,6 +21,7 @@ import Postbox
public enum ChatListNodeMode { public enum ChatListNodeMode {
case chatList case chatList
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool) case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool)
case peerType(type: ReplyMarkupButtonRequestPeerType)
} }
struct ChatListNodeListViewTransition { struct ChatListNodeListViewTransition {
@ -307,10 +308,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData): case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData):
var header: ChatListSearchItemHeader? var header: ChatListSearchItemHeader?
if case .peerType = mode {
} else {
if case .action = appearance { if case .action = appearance {
// TODO: hack, generalize // TODO: hack, generalize
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
} }
}
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
context: context, context: context,
@ -546,6 +550,37 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
animationCache: nodeInteraction.animationCache, animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint) ), 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): case let .HoleEntry(_, theme):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) 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, animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint) ), 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): case let .HoleEntry(_, theme):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint) return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint)
@ -806,10 +872,13 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint) return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData): case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData):
var header: ChatListSearchItemHeader? var header: ChatListSearchItemHeader?
if case .peerType = mode {
} else {
if case .action = appearance { if case .action = appearance {
// TODO: hack, generalize // TODO: hack, generalize
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
} }
}
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem( return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings), presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
context: context, context: context,
@ -963,7 +1032,7 @@ public final class ChatListNode: ListView {
public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
public var contentScrollingEnded: ((ListView) -> Bool)? public var contentScrollingEnded: ((ListView) -> Bool)?
var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)? public var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)?
private var currentIsEmptyState: ChatListNodeEmptyState? private var currentIsEmptyState: ChatListNodeEmptyState?
public var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? public var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
@ -1491,13 +1560,15 @@ public final class ChatListNode: ListView {
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault) 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 (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 { switch entry {
case let .PeerEntry(peerEntry): case let .PeerEntry(peerEntry):
let peer = peerEntry.peer let peer = peerEntry.peer
switch mode { switch mode {
case .chatList: case .chatList:
isEmpty = false
return true return true
case let .peers(filter, _, _, _, _): case let .peers(filter, _, _, _, _):
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false } guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
@ -1601,12 +1672,113 @@ public final class ChatListNode: ListView {
} }
} }
isEmpty = false
return true 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: default:
return true return true
} }
} }
if isEmpty {
entries = []
}
let processedView = ChatListNodeView(originalList: update.list, filteredEntries: entries, isLoading: isLoading, filter: filter) let processedView = ChatListNodeView(originalList: update.list, filteredEntries: entries, isLoading: isLoading, filter: filter)
let previousView = previousView.swap(processedView) let previousView = previousView.swap(processedView)
@ -1849,7 +2021,7 @@ public final class ChatListNode: ListView {
switch mode { switch mode {
case .chatList: case .chatList:
initialLocation = .initial(count: 50, filter: self.chatListFilter) initialLocation = .initial(count: 50, filter: self.chatListFilter)
case .peers: case .peers, .peerType:
initialLocation = .initial(count: 200, filter: self.chatListFilter) initialLocation = .initial(count: 200, filter: self.chatListFilter)
} }
self.setChatListLocation(initialLocation) self.setChatListLocation(initialLocation)

View File

@ -675,12 +675,23 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
result.append(.HeaderEntry) result.append(.HeaderEntry)
} }
if !view.hasLater, case let .peers(_, _, additionalCategories, _, _) = mode { if !view.hasLater {
if case let .peers(_, _, additionalCategories, _, _) = mode {
var index = 0 var index = 0
for category in additionalCategories.reversed() { 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)) 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 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
}
}
} }
} }

View File

@ -345,7 +345,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
Queue.concurrentDefaultQueue().async { Queue.concurrentDefaultQueue().async {
if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) { 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 |> deliverOnMainQueue).start(next: { [weak self] results in
if let strongSelf = self { if let strongSelf = self {
strongSelf.recognizedContentNode?.removeFromSupernode() strongSelf.recognizedContentNode?.removeFromSupernode()

View File

@ -15,6 +15,7 @@ swift_library(
"//submodules/Postbox:Postbox", "//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore", "//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -5,6 +5,7 @@ import SwiftSignalKit
import Postbox import Postbox
import TelegramCore import TelegramCore
import TelegramUIPreferences import TelegramUIPreferences
import AccountContext
private final class CachedImageRecognizedContent: Codable { private final class CachedImageRecognizedContent: Codable {
public let results: [RecognizedContent] 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> { public func recognizedContent(context: AccountContext, image: @escaping () -> UIImage?, messageId: MessageId) -> Signal<[RecognizedContent], NoError> {
return cachedImageRecognizedContent(engine: engine, messageId: messageId) if context.sharedContext.immediateExperimentalUISettings.disableImageContentAnalysis {
return .single([])
}
return cachedImageRecognizedContent(engine: context.engine, messageId: messageId)
|> mapToSignal { cachedContent -> Signal<[RecognizedContent], NoError> in |> mapToSignal { cachedContent -> Signal<[RecognizedContent], NoError> in
if let cachedContent = cachedContent { if let cachedContent = cachedContent {
return .single(cachedContent.results) return .single(cachedContent.results)
@ -343,7 +347,7 @@ public func recognizedContent(engine: TelegramEngine, image: @escaping () -> UII
|> then( |> then(
recognizeContent(in: image()) recognizeContent(in: image())
|> beforeNext { results in |> 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()
} }
) )
} }

View File

@ -66,6 +66,8 @@
- (UIView *)createAnimationSnapshot; - (UIView *)createAnimationSnapshot;
- (UIView *)createTextSnapshot; - (UIView *)createTextSnapshot;
- (void)animateIn;
@property (nonatomic) bool isEnabled; @property (nonatomic) bool isEnabled;
- (void)startTimer; - (void)startTimer;

View File

@ -14,6 +14,9 @@
NSMutableAttributedString *_description; 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; - (id)initWithFrame:(CGRect)frame headline:(NSString*)headline description:(NSString*)description color:(UIColor *)color;
@end @end

View File

@ -29,7 +29,7 @@
headlineLabel.textColor = color; headlineLabel.textColor = color;
headlineLabel.textAlignment = NSTextAlignmentCenter; headlineLabel.textAlignment = NSTextAlignmentCenter;
headlineLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth; headlineLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth;
_headerLabel = headlineLabel;
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineSpacing = IPAD ? 4 : 3; style.lineSpacing = IPAD ? 4 : 3;
@ -76,7 +76,7 @@
descriptionLabel.numberOfLines=0; descriptionLabel.numberOfLines=0;
descriptionLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth; descriptionLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth;
[self addSubview:descriptionLabel]; [self addSubview:descriptionLabel];
_descriptionLabel = descriptionLabel;
[self addSubview:headlineLabel]; [self addSubview:headlineLabel];

View File

@ -102,6 +102,7 @@ typedef enum {
NSDictionary<NSString *, NSString *> *_englishStrings; NSDictionary<NSString *, NSString *> *_englishStrings;
UIView *_wrapperView; UIView *_wrapperView;
UIView *_startButton;
bool _loadedView; bool _loadedView;
} }
@ -124,8 +125,6 @@ typedef enum {
_regularDotColor = regularDotColor; _regularDotColor = regularDotColor;
_highlightedDotColor = highlightedDotColor; _highlightedDotColor = highlightedDotColor;
self.automaticallyAdjustsScrollViewInsets = false;
NSArray<NSString *> *stringKeys = @[ NSArray<NSString *> *stringKeys = @[
@"Tour.Title1", @"Tour.Title1",
@"Tour.Title2", @"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 - (void)loadGL
{ {
#if TARGET_OS_SIMULATOR && defined(__aarch64__) #if TARGET_OS_SIMULATOR && defined(__aarch64__)
@ -313,6 +351,7 @@ typedef enum {
[self.view addSubview:_wrapperView]; [self.view addSubview:_wrapperView];
_pageScrollView = [[UIScrollView alloc]initWithFrame:self.view.bounds]; _pageScrollView = [[UIScrollView alloc]initWithFrame:self.view.bounds];
_pageScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
_pageScrollView.clipsToBounds = true; _pageScrollView.clipsToBounds = true;
_pageScrollView.opaque = true; _pageScrollView.opaque = true;
_pageScrollView.clearsContextBeforeDrawing = false; _pageScrollView.clearsContextBeforeDrawing = false;
@ -512,6 +551,7 @@ typedef enum {
CGFloat startButtonWidth = MIN(430.0 - 48.0, self.view.bounds.size.width - 48.0f); CGFloat startButtonWidth = MIN(430.0 - 48.0, self.view.bounds.size.width - 48.0f);
UIView *startButton = self.createStartButton(startButtonWidth); UIView *startButton = self.createStartButton(startButtonWidth);
if (startButton.superview == nil) { if (startButton.superview == nil) {
_startButton = startButton;
[self.view addSubview: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); startButton.frame = CGRectMake(floor((self.view.bounds.size.width - startButtonWidth) / 2.0f), self.view.bounds.size.height - startButtonY - statusBarHeight, startButtonWidth, 50.0f);
@ -523,8 +563,7 @@ typedef enum {
_pageScrollView.contentSize=CGSizeMake(_headlines.count * self.view.bounds.size.width, 150); _pageScrollView.contentSize=CGSizeMake(_headlines.count * self.view.bounds.size.width, 150);
_pageScrollView.contentOffset = CGPointMake(_currentPage * self.view.bounds.size.width, 0); _pageScrollView.contentOffset = CGPointMake(_currentPage * self.view.bounds.size.width, 0);
[_pageViews enumerateObjectsUsingBlock:^(UIView *pageView, NSUInteger index, __unused BOOL *stop) [_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); pageView.frame = CGRectMake(index * self.view.bounds.size.width, (pageY - statusBarHeight), self.view.bounds.size.width, 150);
}]; }];
} }

View File

@ -512,7 +512,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = [] 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: { c.dismiss(completion: {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in |> deliverOnMainQueue).start(next: { peer in

View File

@ -834,9 +834,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
case .attachMenuBotAllowed: case .attachMenuBotAllowed:
attributedString = NSAttributedString(string: strings.Notification_BotWriteAllowed, font: titleFont, textColor: primaryTextColor) attributedString = NSAttributedString(string: strings.Notification_BotWriteAllowed, font: titleFont, textColor: primaryTextColor)
case let .requestedPeer(_, peerId): case let .requestedPeer(_, peerId):
//TODO:localize let peerName = message.peers[peerId].flatMap(EnginePeer.init)?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""
let _ = peerId attributedString = addAttributesToStringWithRanges(strings.Notification_RequestedPeer(peerName)._tuple, body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, peerId)]))
attributedString = NSAttributedString(string: "Requested Chat", font: titleFont, textColor: primaryTextColor)
case .unknown: case .unknown:
attributedString = nil attributedString = nil
} }

View File

@ -167,6 +167,7 @@ public final class ChatControllerInteraction {
public let openJoinLink: (String) -> Void public let openJoinLink: (String) -> Void
public let openWebView: (String, String, Bool, Bool) -> Void public let openWebView: (String, String, Bool, Bool) -> Void
public let activateAdAction: (EngineMessage.Id) -> Void public let activateAdAction: (EngineMessage.Id) -> Void
public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32) -> Void
public let requestMessageUpdate: (MessageId, Bool) -> Void public let requestMessageUpdate: (MessageId, Bool) -> Void
public let cancelInteractiveKeyboardGestures: () -> Void public let cancelInteractiveKeyboardGestures: () -> Void
@ -277,6 +278,7 @@ public final class ChatControllerInteraction {
openJoinLink: @escaping (String) -> Void, openJoinLink: @escaping (String) -> Void,
openWebView: @escaping (String, String, Bool, Bool) -> Void, openWebView: @escaping (String, String, Bool, Bool) -> Void,
activateAdAction: @escaping (EngineMessage.Id) -> Void, activateAdAction: @escaping (EngineMessage.Id) -> Void,
openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32) -> Void,
requestMessageUpdate: @escaping (MessageId, Bool) -> Void, requestMessageUpdate: @escaping (MessageId, Bool) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void,
dismissTextInput: @escaping () -> Void, dismissTextInput: @escaping () -> Void,
@ -370,6 +372,7 @@ public final class ChatControllerInteraction {
self.openJoinLink = openJoinLink self.openJoinLink = openJoinLink
self.openWebView = openWebView self.openWebView = openWebView
self.activateAdAction = activateAdAction self.activateAdAction = activateAdAction
self.openRequestedPeerSelection = openRequestedPeerSelection
self.requestMessageUpdate = requestMessageUpdate self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
self.dismissTextInput = dismissTextInput self.dismissTextInput = dismissTextInput

View File

@ -35,6 +35,7 @@ import BackgroundTasks
import UIKitRuntimeUtils import UIKitRuntimeUtils
import StoreKit import StoreKit
import PhoneNumberFormat import PhoneNumberFormat
import AuthorizationUI
#if canImport(AppCenter) #if canImport(AppCenter)
import AppCenter import AppCenter
@ -315,6 +316,11 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
self.window = window self.window = window
self.nativeWindow = window self.nativeWindow = window
let launchIconSize = CGSize(width: 120.0, height: 120.0)
let launchIconView = UIImageView(image: UIImage(bundleImageName: "Chat/Links/QrLogo"))
launchIconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((hostView.containerView.frame.width - launchIconSize.width) / 2.0), y: floorToScreenPixels((hostView.containerView.frame.height - launchIconSize.height) / 2.0)), size: launchIconSize)
hostView.containerView.addSubview(launchIconView)
let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
@ -1044,7 +1050,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
self.contextDisposable.set((self.context.get() self.contextDisposable.set((self.context.get()
|> deliverOnMainQueue).start(next: { context in |> deliverOnMainQueue).start(next: { [weak launchIconView] context in
print("Application: context took \(CFAbsoluteTimeGetCurrent() - startTime) to become available") print("Application: context took \(CFAbsoluteTimeGetCurrent() - startTime) to become available")
var network: Network? var network: Network?
@ -1075,8 +1081,15 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
print("Launch to ready took \((CFAbsoluteTimeGetCurrent() - launchStartTime) * 1000.0) ms") print("Launch to ready took \((CFAbsoluteTimeGetCurrent() - launchStartTime) * 1000.0) ms")
self.mainWindow.debugAction = nil self.mainWindow.debugAction = nil
self.mainWindow.viewController = context.rootController self.mainWindow.viewController = context.rootController
if let launchIconView {
launchIconView.alpha = 0.0
launchIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak launchIconView] _ in
launchIconView?.removeFromSuperview()
})
}
if firstTime { if firstTime {
let layer = context.rootController.view.layer let layer = context.rootController.view.layer
layer.allowsGroupOpacity = true layer.allowsGroupOpacity = true
@ -1123,7 +1136,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
let authContextReadyDisposable = MetaDisposable() let authContextReadyDisposable = MetaDisposable()
self.authContextDisposable.set((self.authContext.get() self.authContextDisposable.set((self.authContext.get()
|> deliverOnMainQueue).start(next: { context in |> deliverOnMainQueue).start(next: { [weak launchIconView] context in
var network: Network? var network: Network?
if let context = context { if let context = context {
network = context.account.network network = context.account.network
@ -1155,15 +1168,38 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
self.authContextValue = context self.authContextValue = context
if let context = context { if let context = context {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self.mainWindow.present(statusController, on: .root) 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<Bool, NoError> = context.isReady.get() let isReady: Signal<Bool, NoError> = context.isReady.get()
authContextReadyDisposable.set((isReady authContextReadyDisposable.set((isReady
|> filter { $0 } |> filter { $0 }
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { _ in |> deliverOnMainQueue).start(next: { _ in
statusController.dismiss() progressDisposable.dispose()
self.mainWindow.present(context.rootController, on: .root) 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 { } else {
authContextReadyDisposable.set(nil) authContextReadyDisposable.set(nil)

View File

@ -431,8 +431,10 @@ final class ChatButtonKeyboardInputNode: ChatInputNode {
}) })
case let .openWebView(url, simple): case let .openWebView(url, simple):
self.controllerInteraction.openWebView(markupButton.title, url, simple, false) self.controllerInteraction.openWebView(markupButton.title, url, simple, false)
case .requestPeer: case let .requestPeer(peerType, buttonId):
break if let message = self.message {
self.controllerInteraction.openRequestedPeerSelection(message.id, peerType, buttonId)
}
} }
if dismissIfOnce { if dismissIfOnce {
if let message = self.message { if let message = self.message {

View File

@ -4131,13 +4131,53 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .join(_, joinHash): case let .join(_, joinHash):
self.controllerInteraction?.openJoinLink(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 }, requestMessageUpdate: { [weak self] id, scroll in
if let strongSelf = self { if let self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)
} }
}, cancelInteractiveKeyboardGestures: { [weak self] in }, cancelInteractiveKeyboardGestures: { [weak self] in
(self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() if let self {
self?.chatDisplayNode.cancelInteractiveKeyboardGestures() (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
self.chatDisplayNode.cancelInteractiveKeyboardGestures()
}
}, dismissTextInput: { [weak self] in }, dismissTextInput: { [weak self] in
self?.chatDisplayNode.dismissTextInput() self?.chatDisplayNode.dismissTextInput()
}, scrollToMessageId: { [weak self] index in }, scrollToMessageId: { [weak self] index in

View File

@ -1552,7 +1552,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
if isReplyThreadHead { 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) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in }, action: { c, _ in
c.dismiss(completion: { c.dismiss(completion: {

View File

@ -555,6 +555,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openJoinLink: { _ in }, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in }, openWebView: { _, _, _, _ in
}, activateAdAction: { _ in }, activateAdAction: { _ in
}, openRequestedPeerSelection: { _, _, _ in
}, requestMessageUpdate: { _, _ in }, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: { }, dismissTextInput: {

View File

@ -206,7 +206,13 @@ private func CreateChannelEntries(presentationData: PresentationData, state: Cre
return entries 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 initialState = CreateChannelState(creating: false, editingName: ItemListAvatarAndNameInfoItemName.title(title: "", type: .channel), editingDescriptionText: "", avatar: nil)
let statePromise = ValuePromise(initialState, ignoreRepeated: true) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState) let stateValue = Atomic(value: initialState)

View File

@ -29,6 +29,7 @@ import LegacyMediaPickerUI
import ContextUI import ContextUI
import ChatTimerScreen import ChatTimerScreen
import AsyncDisplayKit import AsyncDisplayKit
import TextFormat
private struct CreateGroupArguments { private struct CreateGroupArguments {
let context: AccountContext let context: AccountContext
@ -39,10 +40,14 @@ private struct CreateGroupArguments {
let changeLocation: () -> Void let changeLocation: () -> Void
let updateWithVenue: (TelegramMediaMap) -> Void let updateWithVenue: (TelegramMediaMap) -> Void
let updateAutoDelete: () -> Void let updateAutoDelete: () -> Void
let updatePublicLinkText: (String) -> Void
let openAuction: (String) -> Void
} }
private enum CreateGroupSection: Int32 { private enum CreateGroupSection: Int32 {
case info case info
case username
case topics
case autoDelete case autoDelete
case members case members
case location case location
@ -65,6 +70,12 @@ private enum CreateGroupEntryTag: ItemListItemTag {
private enum CreateGroupEntry: ItemListNodeEntry { private enum CreateGroupEntry: ItemListNodeEntry {
case groupInfo(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?) case groupInfo(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?)
case setProfilePhoto(PresentationTheme, String) 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 autoDelete(title: String, value: String)
case autoDeleteInfo(String) case autoDeleteInfo(String)
case member(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, PeerPresence?) case member(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, PeerPresence?)
@ -79,6 +90,10 @@ private enum CreateGroupEntry: ItemListNodeEntry {
switch self { switch self {
case .groupInfo, .setProfilePhoto: case .groupInfo, .setProfilePhoto:
return CreateGroupSection.info.rawValue return CreateGroupSection.info.rawValue
case .usernameHeader, .username, .usernameStatus, .usernameInfo:
return CreateGroupSection.username.rawValue
case .topics, .topicsInfo:
return CreateGroupSection.topics.rawValue
case .autoDelete, .autoDeleteInfo: case .autoDelete, .autoDeleteInfo:
return CreateGroupSection.autoDelete.rawValue return CreateGroupSection.autoDelete.rawValue
case .member: case .member:
@ -96,12 +111,24 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return 0 return 0
case .setProfilePhoto: case .setProfilePhoto:
return 1 return 1
case .autoDelete: case .usernameHeader:
return 2 return 2
case .autoDeleteInfo: case .username:
return 3 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, _, _, _, _, _, _): case let .member(index, _, _, _, _, _, _):
return 4 + index return 10 + index
case .locationHeader: case .locationHeader:
return 10000 return 10000
case .location: case .location:
@ -153,6 +180,43 @@ private enum CreateGroupEntry: ItemListNodeEntry {
} else { } else {
return false 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): case let .autoDelete(title, value):
if case .autoDelete(title, value) = rhs { if case .autoDelete(title, value) = rhs {
return true 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: { return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.changeProfilePhoto() 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): case let .autoDelete(text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
arguments.updateAutoDelete() arguments.updateAutoDelete()
@ -299,9 +398,11 @@ private struct CreateGroupState: Equatable {
var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar? var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?
var location: PeerGeoLocation? var location: PeerGeoLocation?
var autoremoveTimeout: Int32? 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] = [] var entries: [CreateGroupEntry] = []
let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil)
@ -310,6 +411,58 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat
entries.append(.groupInfo(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, groupInfoState, state.avatar)) entries.append(.groupInfo(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, groupInfoState, state.avatar))
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 {
let autoremoveTimeout = state.autoremoveTimeout ?? globalAutoremoveTimeout let autoremoveTimeout = state.autoremoveTimeout ?? globalAutoremoveTimeout
let autoRemoveText: String let autoRemoveText: String
if autoremoveTimeout == 0 { if autoremoveTimeout == 0 {
@ -319,6 +472,7 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat
} }
entries.append(.autoDelete(title: presentationData.strings.CreateGroup_AutoDeleteTitle, value: autoRemoveText)) entries.append(.autoDelete(title: presentationData.strings.CreateGroup_AutoDeleteTitle, value: autoRemoveText))
entries.append(.autoDeleteInfo(presentationData.strings.CreateGroup_AutoDeleteText)) entries.append(.autoDeleteInfo(presentationData.strings.CreateGroup_AutoDeleteText))
}
var peers: [Peer] = [] var peers: [Peer] = []
for peerId in peerIds { for peerId in peerIds {
@ -382,7 +536,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
location = PeerGeoLocation(latitude: latitude, longitude: longitude, address: address ?? "") 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 statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState) let stateValue = Atomic(value: initialState)
let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in
@ -400,6 +554,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
let checkAddressNameDisposable = MetaDisposable()
actionsDisposable.add(checkAddressNameDisposable)
let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil) let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
let uploadedAvatar = Promise<UploadedPeerPhotoData>() let uploadedAvatar = Promise<UploadedPeerPhotoData>()
@ -429,8 +586,8 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
return current return current
} }
}, done: { }, done: {
let (creating, title, location) = stateValue.with { state -> (Bool, String, PeerGeoLocation?) in let (creating, title, location, publicLink) = stateValue.with { state -> (Bool, String, PeerGeoLocation?, String?) in
return (state.creating, state.editingName.composedTitle, state.location) return (state.creating, state.editingName.composedTitle, state.location, state.editingPublicLinkText)
} }
if !creating && !title.isEmpty { if !creating && !title.isEmpty {
@ -447,7 +604,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
let autoremoveTimeout = stateValue.with({ $0 }).autoremoveTimeout ?? globalAutoremoveTimeout let autoremoveTimeout = stateValue.with({ $0 }).autoremoveTimeout ?? globalAutoremoveTimeout
let ttlPeriod: Int32? = autoremoveTimeout == 0 ? nil : autoremoveTimeout let ttlPeriod: Int32? = autoremoveTimeout == 0 ? nil : autoremoveTimeout
let createSignal: Signal<PeerId?, CreateGroupError> var createSignal: Signal<PeerId?, CreateGroupError>
switch mode { switch mode {
case .generic: case .generic:
createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds, ttlPeriod: ttlPeriod) createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds, ttlPeriod: ttlPeriod)
@ -496,6 +653,81 @@ 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 actionsDisposable.add((createSignal
@ -910,8 +1142,42 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
presentInGlobalOverlay?(contextController) 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(), let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData, context.sharedContext.presentationData,
statePromise.get(), statePromise.get(),
@ -921,7 +1187,6 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.GlobalAutoremoveTimeout()) context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.GlobalAutoremoveTimeout())
) )
|> map { presentationData, state, view, address, venues, globalAutoremoveTimeout -> (ItemListControllerState, (ItemListNodeState, Any)) in |> map { presentationData, state, view, address, venues, globalAutoremoveTimeout -> (ItemListControllerState, (ItemListNodeState, Any)) in
let rightNavigationButton: ItemListNavigationButton let rightNavigationButton: ItemListNavigationButton
if state.creating { if state.creating {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) 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 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)) return (controllerState, (listState, arguments))
} }
@ -941,6 +1206,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
} }
let controller = ItemListController(context: context, state: signal) let controller = ItemListController(context: context, state: signal)
controller.beganInteractiveDragging = {
endEditingImpl?()
}
replaceControllerImpl = { [weak controller] value in replaceControllerImpl = { [weak controller] value in
(controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true)
} }

View File

@ -158,6 +158,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, openJoinLink: { _ in }, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in }, openWebView: { _, _, _, _ in
}, activateAdAction: { _ in }, activateAdAction: { _ in
}, openRequestedPeerSelection: { _, _, _ in
}, requestMessageUpdate: { _, _ in }, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: { }, dismissTextInput: {

View File

@ -2728,6 +2728,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}, openJoinLink: { _ in }, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in }, openWebView: { _, _, _, _ in
}, activateAdAction: { _ in }, activateAdAction: { _ in
}, openRequestedPeerSelection: { _, _, _ in
}, requestMessageUpdate: { _, _ in }, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: { }, dismissTextInput: {
@ -3094,7 +3095,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
} }
} }
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<Void, NoError> = .complete() var updateNameSignal: Signal<Void, NoError> = .complete()
var hasProgress = false var hasProgress = false
if peer.firstName != firstName || peer.lastName != lastName { if peer.firstName != firstName || peer.lastName != lastName {

View File

@ -62,6 +62,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
private let pretendPresentedInModal: Bool private let pretendPresentedInModal: Bool
private let forwardedMessageIds: [EngineMessage.Id] private let forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool private let hasTypeHeaders: Bool
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
override public var _presentedInModal: Bool { override public var _presentedInModal: Bool {
get { get {
@ -93,12 +94,29 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
self.forwardedMessageIds = params.forwardedMessageIds self.forwardedMessageIds = params.forwardedMessageIds
self.hasTypeHeaders = params.hasTypeHeaders self.hasTypeHeaders = params.hasTypeHeaders
self.selectForumThreads = params.selectForumThreads self.selectForumThreads = params.selectForumThreads
self.requestPeerType = params.requestPeerType
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.customTitle = params.title 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 self.title = self.customTitle ?? self.presentationData.strings.Conversation_ForwardTitle
if params.forumPeerId == nil { if params.forumPeerId == nil {
@ -159,7 +177,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
} }
override public func loadDisplayNode() { 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) self?.present(c, in: .window(.root), with: a)
}, presentInGlobalOverlay: { [weak self] c, a in }, presentInGlobalOverlay: { [weak self] c, a in
self?.presentInGlobalOverlay(c, with: a) self?.presentInGlobalOverlay(c, with: a)

View File

@ -18,9 +18,13 @@ import ChatSendMessageActionUI
import ChatTextLinkEditUI import ChatTextLinkEditUI
import AnimationCache import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
final class PeerSelectionControllerNode: ASDisplayNode { final class PeerSelectionControllerNode: ASDisplayNode {
private let context: AccountContext private let context: AccountContext
private weak var controller: PeerSelectionController?
private let present: (ViewController, Any?) -> Void private let present: (ViewController, Any?) -> Void
private let presentInGlobalOverlay: (ViewController, Any?) -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void
private let dismiss: () -> Void private let dismiss: () -> Void
@ -29,6 +33,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
private let hasGlobalSearch: Bool private let hasGlobalSearch: Bool
private let forwardedMessageIds: [EngineMessage.Id] private let forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool private let hasTypeHeaders: Bool
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private var presentationInterfaceState: ChatPresentationInterfaceState private var presentationInterfaceState: ChatPresentationInterfaceState
private var interfaceInteraction: ChatPanelInterfaceInteraction? private var interfaceInteraction: ChatPanelInterfaceInteraction?
@ -41,6 +46,16 @@ final class PeerSelectionControllerNode: ASDisplayNode {
var navigationBar: NavigationBar? 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 toolbarBackgroundNode: NavigationBackgroundNode?
private let toolbarSeparatorNode: ASDisplayNode? private let toolbarSeparatorNode: ASDisplayNode?
private let segmentedControlNode: SegmentedControlNode? private let segmentedControlNode: SegmentedControlNode?
@ -83,12 +98,15 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return self.readyValue.get() return self.readyValue.get()
} }
private var isEmpty = false
private var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) { private var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) {
return (self.presentationData, self.presentationDataPromise.get()) 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.context = context
self.controller = controller
self.present = present self.present = present
self.presentInGlobalOverlay = presentInGlobalOverlay self.presentInGlobalOverlay = presentInGlobalOverlay
self.dismiss = dismiss self.dismiss = dismiss
@ -97,6 +115,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.hasGlobalSearch = hasGlobalSearch self.hasGlobalSearch = hasGlobalSearch
self.forwardedMessageIds = forwardedMessageIds self.forwardedMessageIds = forwardedMessageIds
self.hasTypeHeaders = hasTypeHeaders self.hasTypeHeaders = hasTypeHeaders
self.requestPeerType = requestPeerType
self.presentationData = presentationData self.presentationData = presentationData
@ -107,6 +126,43 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } 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 { if hasChatListSelector && hasContactSelector {
self.toolbarBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) self.toolbarBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
@ -137,7 +193,14 @@ final class PeerSelectionControllerNode: ASDisplayNode {
chatListLocation = .chatList(groupId: .root) chatListLocation = .chatList(groupId: .root)
} }
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) 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: 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() super.init()
@ -184,6 +247,17 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return self?.contentScrollingEnded?(listView) ?? false 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) self.addSubnode(self.chatListNode)
if hasChatListSelector && hasContactSelector { if hasChatListSelector && hasContactSelector {
@ -196,6 +270,17 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.addSubnode(self.segmentedControlNode!) 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 { if !hasChatListSelector && hasContactSelector {
self.indexChanged(1) self.indexChanged(1)
} }
@ -484,6 +569,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.updateChatPresentationInterfaceState({ $0.updatedTheme(self.presentationData.theme) }) 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.toolbarBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.segmentedControlNode?.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme)) 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.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) 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 (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) 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, animationRenderer: self.animationRenderer,
updatedPresentationData: self.updatedPresentationData, updatedPresentationData: self.updatedPresentationData,
filter: self.filter, filter: self.filter,
requestPeerType: self.requestPeerType,
location: chatListLocation, location: chatListLocation,
displaySearchFilters: false, displaySearchFilters: false,
hasDownloads: 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"))
}
}

View File

@ -1379,6 +1379,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, openJoinLink: { _ in }, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in }, openWebView: { _, _, _, _ in
}, activateAdAction: { _ in }, activateAdAction: { _ in
}, openRequestedPeerSelection: { _, _, _ in
}, requestMessageUpdate: { _, _ in }, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: { }, dismissTextInput: {

View File

@ -41,7 +41,6 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var enableDebugDataDisplay: Bool public var enableDebugDataDisplay: Bool
public var acceleratedStickers: Bool public var acceleratedStickers: Bool
public var experimentalBackground: Bool public var experimentalBackground: Bool
public var snow: Bool
public var inlineStickers: Bool public var inlineStickers: Bool
public var localTranscription: Bool public var localTranscription: Bool
public var enableReactionOverrides: Bool public var enableReactionOverrides: Bool
@ -49,6 +48,9 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var accountReactionEffectOverrides: [AccountReactionOverrides] public var accountReactionEffectOverrides: [AccountReactionOverrides]
public var accountStickerEffectOverrides: [AccountReactionOverrides] public var accountStickerEffectOverrides: [AccountReactionOverrides]
public var disableQuickReaction: Bool public var disableQuickReaction: Bool
public var disableLanguageRecognition: Bool
public var disableImageContentAnalysis: Bool
public var disableBackgroundAnimation: Bool
public static var defaultSettings: ExperimentalUISettings { public static var defaultSettings: ExperimentalUISettings {
return ExperimentalUISettings( return ExperimentalUISettings(
@ -67,14 +69,16 @@ public struct ExperimentalUISettings: Codable, Equatable {
enableDebugDataDisplay: false, enableDebugDataDisplay: false,
acceleratedStickers: false, acceleratedStickers: false,
experimentalBackground: false, experimentalBackground: false,
snow: false,
inlineStickers: false, inlineStickers: false,
localTranscription: false, localTranscription: false,
enableReactionOverrides: false, enableReactionOverrides: false,
inlineForums: false, inlineForums: false,
accountReactionEffectOverrides: [], accountReactionEffectOverrides: [],
accountStickerEffectOverrides: [], accountStickerEffectOverrides: [],
disableQuickReaction: false disableQuickReaction: false,
disableLanguageRecognition: false,
disableImageContentAnalysis: false,
disableBackgroundAnimation: false
) )
} }
@ -94,14 +98,16 @@ public struct ExperimentalUISettings: Codable, Equatable {
enableDebugDataDisplay: Bool, enableDebugDataDisplay: Bool,
acceleratedStickers: Bool, acceleratedStickers: Bool,
experimentalBackground: Bool, experimentalBackground: Bool,
snow: Bool,
inlineStickers: Bool, inlineStickers: Bool,
localTranscription: Bool, localTranscription: Bool,
enableReactionOverrides: Bool, enableReactionOverrides: Bool,
inlineForums: Bool, inlineForums: Bool,
accountReactionEffectOverrides: [AccountReactionOverrides], accountReactionEffectOverrides: [AccountReactionOverrides],
accountStickerEffectOverrides: [AccountReactionOverrides], accountStickerEffectOverrides: [AccountReactionOverrides],
disableQuickReaction: Bool disableQuickReaction: Bool,
disableLanguageRecognition: Bool,
disableImageContentAnalysis: Bool,
disableBackgroundAnimation: Bool
) { ) {
self.keepChatNavigationStack = keepChatNavigationStack self.keepChatNavigationStack = keepChatNavigationStack
self.skipReadHistory = skipReadHistory self.skipReadHistory = skipReadHistory
@ -118,7 +124,6 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.enableDebugDataDisplay = enableDebugDataDisplay self.enableDebugDataDisplay = enableDebugDataDisplay
self.acceleratedStickers = acceleratedStickers self.acceleratedStickers = acceleratedStickers
self.experimentalBackground = experimentalBackground self.experimentalBackground = experimentalBackground
self.snow = snow
self.inlineStickers = inlineStickers self.inlineStickers = inlineStickers
self.localTranscription = localTranscription self.localTranscription = localTranscription
self.enableReactionOverrides = enableReactionOverrides self.enableReactionOverrides = enableReactionOverrides
@ -126,6 +131,9 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.accountReactionEffectOverrides = accountReactionEffectOverrides self.accountReactionEffectOverrides = accountReactionEffectOverrides
self.accountStickerEffectOverrides = accountStickerEffectOverrides self.accountStickerEffectOverrides = accountStickerEffectOverrides
self.disableQuickReaction = disableQuickReaction self.disableQuickReaction = disableQuickReaction
self.disableLanguageRecognition = disableLanguageRecognition
self.disableImageContentAnalysis = disableImageContentAnalysis
self.disableBackgroundAnimation = disableBackgroundAnimation
} }
public init(from decoder: Decoder) throws { 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.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0
self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 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.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.inlineStickers = (try container.decodeIfPresent(Int32.self, forKey: "inlineStickers") ?? 0) != 0
self.localTranscription = (try container.decodeIfPresent(Int32.self, forKey: "localTranscription") ?? 0) != 0 self.localTranscription = (try container.decodeIfPresent(Int32.self, forKey: "localTranscription") ?? 0) != 0
self.enableReactionOverrides = try container.decodeIfPresent(Bool.self, forKey: "enableReactionOverrides") ?? false 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.accountReactionEffectOverrides = (try? container.decodeIfPresent([AccountReactionOverrides].self, forKey: "accountReactionEffectOverrides")) ?? []
self.accountStickerEffectOverrides = (try? container.decodeIfPresent([AccountReactionOverrides].self, forKey: "accountStickerEffectOverrides")) ?? [] self.accountStickerEffectOverrides = (try? container.decodeIfPresent([AccountReactionOverrides].self, forKey: "accountStickerEffectOverrides")) ?? []
self.disableQuickReaction = try container.decodeIfPresent(Bool.self, forKey: "disableQuickReaction") ?? false 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 { 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.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay")
try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers") 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.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.inlineStickers ? 1 : 0) as Int32, forKey: "inlineStickers")
try container.encode((self.localTranscription ? 1 : 0) as Int32, forKey: "localTranscription") try container.encode((self.localTranscription ? 1 : 0) as Int32, forKey: "localTranscription")
try container.encode(self.enableReactionOverrides, forKey: "enableReactionOverrides") 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.accountReactionEffectOverrides, forKey: "accountReactionEffectOverrides")
try container.encode(self.accountStickerEffectOverrides, forKey: "accountStickerEffectOverrides") try container.encode(self.accountStickerEffectOverrides, forKey: "accountStickerEffectOverrides")
try container.encode(self.disableQuickReaction, forKey: "disableQuickReaction") 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")
} }
} }

View File

@ -147,6 +147,10 @@ public func canTranslateText(context: AccountContext, text: String, showTranslat
} }
if #available(iOS 12.0, *) { if #available(iOS 12.0, *) {
if context.sharedContext.immediateExperimentalUISettings.disableLanguageRecognition {
return (true, nil)
}
var dontTranslateLanguages: [String] = [] var dontTranslateLanguages: [String] = []
if let ignoredLanguages = ignoredLanguages { if let ignoredLanguages = ignoredLanguages {
dontTranslateLanguages = ignoredLanguages dontTranslateLanguages = ignoredLanguages