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"?>
<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"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<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"/>
<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"/>
</view>
</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>

View File

@ -8624,3 +8624,69 @@ Sorry for the inconvenience.";
"StorageManagement.OpenFile" = "Open File";
"ChatListFilter.AddChatsSearchPlaceholder" = "Search Chats";
"RequestPeer.ChooseUserTitle" = "Choose User";
"RequestPeer.ChooseBotTitle" = "Choose Bot";
"RequestPeer.ChooseGroupTitle" = "Choose Group";
"RequestPeer.ChooseChannelTitle" = "Choose Channel";
"RequestPeer.Requirements" = "Requirements:";
"RequestPeer.CreateNewGroup" = "Create a New Group for This";
"RequestPeer.CreateNewChannel" = "Create a New Channel for This";
"RequestPeer.UsersEmpty" = "You don't have users that meet the following requirements:";
"RequestPeer.UsersAllEmpty" = "You don't have any users.";
"RequestPeer.BotsAllEmpty" = "You don't have any bots.";
"RequestPeer.GroupsEmpty" = "You don't have groups that meet the following requirements:";
"RequestPeer.GroupsAllEmpty" = "You don't have any groups.";
"RequestPeer.ChannelsEmpty" = "You don't have channels that meet the following requirements:";
"RequestPeer.ChannelsAllEmpty" = "You don't have any channels.";
"RequestPeer.Requirement.UserPremiumOff" = "User should not have a Premium subscription.";
"RequestPeer.Requirement.UserPremiumOn" = "User should have a Premium subscription.";
"RequestPeer.Requirement.Group.HasUsernameOff" = "The group should be private.";
"RequestPeer.Requirement.Group.HasUsernameOn" = "The group should be public.";
"RequestPeer.Requirement.Group.ForumOff" = "The group should have topics off.";
"RequestPeer.Requirement.Group.ForumOn" = "The group should have topics on.";
"RequestPeer.Requirement.Group.CreatorOn" = "You should be the owner of the group.";
"RequestPeer.Requirement.Group.Rights" = "You have the admin rights to %@.";
"RequestPeer.Requirement.Group.Rights.Info" = "change group info";
"RequestPeer.Requirement.Group.Rights.Send" = "post messages";
"RequestPeer.Requirement.Group.Rights.Delete" = "delete messages";
"RequestPeer.Requirement.Group.Rights.Edit" = "delete messages";
"RequestPeer.Requirement.Group.Rights.Ban" = "ban users";
"RequestPeer.Requirement.Group.Rights.Invite" = "invite users via link";
"RequestPeer.Requirement.Group.Rights.Pin" = "pin messages";
"RequestPeer.Requirement.Group.Rights.Topics" = "manage topics";
"RequestPeer.Requirement.Group.Rights.VideoChats" = "manage video chats";
"RequestPeer.Requirement.Group.Rights.Anonymous" = "remain anonymous";
"RequestPeer.Requirement.Group.Rights.AddAdmins" = "add new admins";
"RequestPeer.Requirement.Channel.HasUsernameOff" = "The channel should be private.";
"RequestPeer.Requirement.Channel.HasUsernameOn" = "The channel should be public.";
"RequestPeer.Requirement.Channel.CreatorOn" = "You should be the owner of the channel.";
"RequestPeer.Requirement.Channel.Rights" = "You have the admin rights to %@.";
"RequestPeer.Requirement.Channel.Rights.Info" = "change group info";
"RequestPeer.Requirement.Channel.Rights.Send" = "post messages";
"RequestPeer.Requirement.Channel.Rights.Delete" = "delete messages";
"RequestPeer.Requirement.Channel.Rights.Edit" = "delete messages";
"RequestPeer.Requirement.Channel.Rights.Ban" = "ban users";
"RequestPeer.Requirement.Channel.Rights.Invite" = "invite users via link";
"RequestPeer.Requirement.Channel.Rights.Pin" = "pin messages";
"RequestPeer.Requirement.Channel.Rights.Topics" = "manage topics";
"RequestPeer.Requirement.Channel.Rights.VideoChats" = "manage video chats";
"RequestPeer.Requirement.Channel.Rights.Anonymous" = "remain anonymous";
"RequestPeer.Requirement.Channel.Rights.AddAdmins" = "add new admins";
"CreateGroup.PublicLinkTitle" = "SET A PUBLIC LINK";
"CreateGroup.PublicLinkInfo" = "You can use **a-z**, **0-9** and underscores. Minimum length is **5** characters.";
"Notification.RequestedPeer" = "You shared %@ with the bot.";
"Conversation.ViewInChannel" = "View in Channel";

View File

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

View File

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

View File

@ -1117,7 +1117,10 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
let wasEmpty = self.viewControllers.isEmpty
super.setViewControllers(viewControllers, animated: animated)
if wasEmpty {
self.topViewController?.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if self.topViewController is AuthorizationSequenceSplashController {
} else {
self.topViewController?.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
}
if !self.didSetReady {
self.didSetReady = true
@ -1150,7 +1153,13 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
}
private func animateIn() {
self.view.layer.animatePosition(from: CGPoint(x: self.view.layer.position.x, y: self.view.layer.position.y + self.view.layer.bounds.size.height), to: self.view.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
if !self.otherAccountPhoneNumbers.1.isEmpty {
self.view.layer.animatePosition(from: CGPoint(x: self.view.layer.position.x, y: self.view.layer.position.y + self.view.layer.bounds.size.height), to: self.view.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
} else {
if let splashController = self.topViewController as? AuthorizationSequenceSplashController {
splashController.animateIn()
}
}
}
private func animateOut(completion: (() -> Void)? = nil) {

View File

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

View File

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

View File

@ -1535,7 +1535,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
//filter.insert(.excludeRecent)
}
let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, location: effectiveLocation, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in
let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, requestPeerType: nil, location: effectiveLocation, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in
self?.requestOpenPeerFromSearch?(peer, threadId, dismissSearch)
}, openDisabledPeer: { _, _ in
}, openRecentPeerOptions: { [weak self] peer in

View File

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

View File

@ -933,6 +933,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let animationRenderer: MultiAnimationRenderer
private let interaction: ChatListSearchInteraction
private let peersFilter: ChatListNodePeersFilter
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private var presentationData: PresentationData
private let key: ChatListSearchPaneKey
private let tagMask: EngineMessage.Tags?
@ -981,7 +982,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let emptyResultsTitleNode: ImmediateTextNode
private let emptyResultsTextNode: ImmediateTextNode
private let emptyResultsAnimationNode: AnimatedStickerNode
private var emptyResultsAnimationSize: CGSize = CGSize()
private var emptyResultsAnimationSize = CGSize()
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData)?
@ -1002,7 +1003,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private var hiddenMediaDisposable: Disposable?
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<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.animationCache = animationCache
self.animationRenderer = animationRenderer
@ -1018,6 +1019,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
peersFilter.insert(.excludeRecent)
}
self.peersFilter = peersFilter
self.requestPeerType = requestPeerType
let tagMask: EngineMessage.Tags?
switch key {
@ -1645,33 +1647,124 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let _ = currentRemotePeers.swap((foundRemotePeers.0, foundRemotePeers.1))
let filteredPeer: (EnginePeer, EnginePeer) -> Bool = { peer, accountPeer in
guard !peersFilter.contains(.excludeSavedMessages) || peer.id != accountPeer.id else { return false }
guard !peersFilter.contains(.excludeSecretChats) || peer.id.namespace != Namespaces.Peer.SecretChat else { return false }
guard !peersFilter.contains(.onlyPrivateChats) || peer.id.namespace == Namespaces.Peer.CloudUser else { return false }
if peersFilter.contains(.onlyGroups) {
var isGroup: Bool = false
if case let .channel(peer) = peer, case .group = peer.info {
isGroup = true
} else if peer.id.namespace == Namespaces.Peer.CloudGroup {
isGroup = true
if let peerType = requestPeerType {
switch peerType {
case let .user(userType):
if case let .user(user) = peer {
if let isBot = userType.isBot {
if isBot != (user.botInfo != nil) {
return false
}
}
if let isPremium = userType.isPremium {
if isPremium != user.isPremium {
return false
}
}
return true
} else {
return false
}
case let .group(groupType):
if case let .legacyGroup(group) = peer {
if groupType.isCreator {
if case .creator = group.role {
} else {
return false
}
}
if let isForum = groupType.isForum, isForum {
return false
}
if let hasUsername = groupType.hasUsername, hasUsername {
return false
}
if let userAdminRights = groupType.userAdminRights {
if case let .admin(rights, _) = group.role {
if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights {
return false
}
} else if case .member = group.role {
return false
}
}
return true
} else if case let .channel(channel) = peer, case .group = channel.info {
if groupType.isCreator {
if !channel.flags.contains(.isCreator) {
return false
}
}
if let isForum = groupType.isForum, isForum {
if isForum != channel.flags.contains(.isForum) {
return false
}
}
if let hasUsername = groupType.hasUsername, hasUsername {
if hasUsername != (channel.addressName != nil) {
return false
}
}
if let userAdminRights = groupType.userAdminRights {
if channel.flags.contains(.isCreator) {
} else if let rights = channel.adminRights {
if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights {
return false
}
} else {
return false
}
}
return true
} else {
return false
}
case let .channel(channelType):
if case let .channel(channel) = peer, case .broadcast = channel.info {
if channelType.isCreator {
if !channel.flags.contains(.isCreator) {
return false
}
}
if let hasUsername = channelType.hasUsername, hasUsername {
if hasUsername != (channel.addressName != nil) {
return false
}
}
return true
} else {
return false
}
}
if !isGroup {
return false
} else {
guard !peersFilter.contains(.excludeSavedMessages) || peer.id != accountPeer.id else { return false }
guard !peersFilter.contains(.excludeSecretChats) || peer.id.namespace != Namespaces.Peer.SecretChat else { return false }
guard !peersFilter.contains(.onlyPrivateChats) || peer.id.namespace == Namespaces.Peer.CloudUser else { return false }
if peersFilter.contains(.onlyGroups) {
var isGroup: Bool = false
if case let .channel(peer) = peer, case .group = peer.info {
isGroup = true
} else if peer.id.namespace == Namespaces.Peer.CloudGroup {
isGroup = true
}
if !isGroup {
return false
}
}
}
if peersFilter.contains(.onlyChannels) {
if case let .channel(peer) = peer, case .broadcast = peer.info {
return true
} else {
return false
if peersFilter.contains(.onlyChannels) {
if case let .channel(peer) = peer, case .broadcast = peer.info {
return true
} else {
return false
}
}
}
if peersFilter.contains(.excludeChannels) {
if case let .channel(peer) = peer, case .broadcast = peer.info {
return false
if peersFilter.contains(.excludeChannels) {
if case let .channel(peer) = peer, case .broadcast = peer.info {
return false
}
}
}

View File

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

View File

@ -21,6 +21,7 @@ import Postbox
public enum ChatListNodeMode {
case chatList
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool)
case peerType(type: ReplyMarkupButtonRequestPeerType)
}
struct ChatListNodeListViewTransition {
@ -307,9 +308,12 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .AdditionalCategory(_, id, title, image, appearance, selected, presentationData):
var header: ChatListSearchItemHeader?
if case .action = appearance {
// TODO: hack, generalize
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
if case .peerType = mode {
} else {
if case .action = appearance {
// TODO: hack, generalize
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
}
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
@ -546,6 +550,37 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint)
case .peerType:
let itemPeer = peer.chatMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
chatPeer = peer
}
let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer)
let status: ContactsPeerItemStatus = .none
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch,
peer: peerContent,
status: status,
enabled: true,
selection: .none,
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
index: nil,
header: nil,
action: { _ in
if let chatPeer = chatPeer {
nodeInteraction.peerSelected(chatPeer, nil, nil, nil)
}
}, disabledAction: nil,
animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint)
}
case let .HoleEntry(_, theme):
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint)
@ -766,6 +801,37 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint)
case .peerType:
let itemPeer = peer.chatMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
chatPeer = peer
}
let peerContent: ContactsPeerItemPeer = .peer(peer: itemPeer, chatPeer: chatPeer)
let status: ContactsPeerItemStatus = .none
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
sortOrder: presentationData.nameSortOrder,
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .generalSearch,
peer: peerContent,
status: status,
enabled: true,
selection: .none,
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
index: nil,
header: nil,
action: { _ in
if let chatPeer = chatPeer {
nodeInteraction.peerSelected(chatPeer, nil, nil, nil)
}
}, disabledAction: nil,
animationCache: nodeInteraction.animationCache,
animationRenderer: nodeInteraction.animationRenderer
), directionHint: entry.directionHint)
}
case let .HoleEntry(_, theme):
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(theme: theme), directionHint: entry.directionHint)
@ -806,9 +872,12 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyHeaderItem(), directionHint: entry.directionHint)
case let .AdditionalCategory(index: _, id, title, image, appearance, selected, presentationData):
var header: ChatListSearchItemHeader?
if case .action = appearance {
// TODO: hack, generalize
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
if case .peerType = mode {
} else {
if case .action = appearance {
// TODO: hack, generalize
header = ChatListSearchItemHeader(type: .orImportIntoAnExistingGroup, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
}
}
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListAdditionalCategoryItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings),
@ -963,7 +1032,7 @@ public final class ChatListNode: ListView {
public var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)?
public var contentScrollingEnded: ((ListView) -> Bool)?
var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)?
public var isEmptyUpdated: ((ChatListNodeEmptyState, Bool, ContainedViewLayoutTransition) -> Void)?
private var currentIsEmptyState: ChatListNodeEmptyState?
public var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
@ -1491,13 +1560,15 @@ public final class ChatListNode: ListView {
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, storageInfo: storageInfo, suggestPasswordSetup: suggestPasswordSetup, mode: mode, chatListLocation: location)
let entries = rawEntries.filter { entry in
var isEmpty = true
var entries = rawEntries.filter { entry in
switch entry {
case let .PeerEntry(peerEntry):
let peer = peerEntry.peer
switch mode {
case .chatList:
isEmpty = false
return true
case let .peers(filter, _, _, _, _):
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
@ -1601,12 +1672,113 @@ public final class ChatListNode: ListView {
}
}
isEmpty = false
return true
case let .peerType(peerType):
if let peer = peer.peer {
switch peerType {
case let .user(userType):
if case let .user(user) = peer {
if let isBot = userType.isBot {
if isBot != (user.botInfo != nil) {
return false
}
}
if let isPremium = userType.isPremium {
if isPremium != user.isPremium {
return false
}
}
isEmpty = false
return true
} else {
return false
}
case let .group(groupType):
if case let .legacyGroup(group) = peer {
if groupType.isCreator {
if case .creator = group.role {
} else {
return false
}
}
if let isForum = groupType.isForum, isForum {
return false
}
if let hasUsername = groupType.hasUsername, hasUsername {
return false
}
if let userAdminRights = groupType.userAdminRights {
if case let .admin(rights, _) = group.role {
if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights {
return false
}
} else if case .member = group.role {
return false
}
}
isEmpty = false
return true
} else if case let .channel(channel) = peer, case .group = channel.info {
if groupType.isCreator {
if !channel.flags.contains(.isCreator) {
return false
}
}
if let isForum = groupType.isForum, isForum {
if isForum != channel.flags.contains(.isForum) {
return false
}
}
if let hasUsername = groupType.hasUsername, hasUsername {
if hasUsername != (channel.addressName != nil) {
return false
}
}
if let userAdminRights = groupType.userAdminRights {
if channel.flags.contains(.isCreator) {
} else if let rights = channel.adminRights {
if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights {
return false
}
} else {
return false
}
}
isEmpty = false
return true
} else {
return false
}
case let .channel(channelType):
if case let .channel(channel) = peer, case .broadcast = channel.info {
if channelType.isCreator {
if !channel.flags.contains(.isCreator) {
return false
}
}
if let hasUsername = channelType.hasUsername, hasUsername {
if hasUsername != (channel.addressName != nil) {
return false
}
}
isEmpty = false
return true
} else {
return false
}
}
} else {
return false
}
}
default:
return true
}
}
if isEmpty {
entries = []
}
let processedView = ChatListNodeView(originalList: update.list, filteredEntries: entries, isLoading: isLoading, filter: filter)
let previousView = previousView.swap(processedView)
@ -1849,7 +2021,7 @@ public final class ChatListNode: ListView {
switch mode {
case .chatList:
initialLocation = .initial(count: 50, filter: self.chatListFilter)
case .peers:
case .peers, .peerType:
initialLocation = .initial(count: 200, filter: self.chatListFilter)
}
self.setChatListLocation(initialLocation)

View File

@ -675,11 +675,22 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
result.append(.HeaderEntry)
}
if !view.hasLater, case let .peers(_, _, additionalCategories, _, _) = mode {
var index = 0
for category in additionalCategories.reversed() {
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
index += 1
if !view.hasLater {
if case let .peers(_, _, additionalCategories, _, _) = mode {
var index = 0
for category in additionalCategories.reversed() {
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
index += 1
}
} else if case let .peerType(type) = mode, !result.isEmpty {
switch type {
case .group:
result.append(.AdditionalCategory(index: 0, id: 0, title: "Create a New Group for This", image: PresentationResourcesItemList.createGroupIcon(state.presentationData.theme), appearance: .action, selected: false, presentationData: state.presentationData))
case .channel:
result.append(.AdditionalCategory(index: 0, id: 0, title: "Create a New Channel for This", image: PresentationResourcesItemList.createGroupIcon(state.presentationData.theme), appearance: .action, selected: false, presentationData: state.presentationData))
default:
break
}
}
}
}

View File

@ -345,7 +345,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
Queue.concurrentDefaultQueue().async {
if let message = strongSelf.message, !message.isCopyProtected() && !imageReference.media.flags.contains(.hasStickers) {
strongSelf.recognitionDisposable.set((recognizedContent(engine: strongSelf.context.engine, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id)
strongSelf.recognitionDisposable.set((recognizedContent(context: strongSelf.context, image: { return generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() }, messageId: message.id)
|> deliverOnMainQueue).start(next: { [weak self] results in
if let strongSelf = self {
strongSelf.recognizedContentNode?.removeFromSupernode()

View File

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

View File

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

View File

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

View File

@ -14,6 +14,9 @@
NSMutableAttributedString *_description;
}
@property (nonatomic, readonly) UILabel *headerLabel;
@property (nonatomic, readonly) UILabel *descriptionLabel;
- (id)initWithFrame:(CGRect)frame headline:(NSString*)headline description:(NSString*)description color:(UIColor *)color;
@end

View File

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

View File

@ -102,6 +102,7 @@ typedef enum {
NSDictionary<NSString *, NSString *> *_englishStrings;
UIView *_wrapperView;
UIView *_startButton;
bool _loadedView;
}
@ -123,9 +124,7 @@ typedef enum {
_accentColor = accentColor;
_regularDotColor = regularDotColor;
_highlightedDotColor = highlightedDotColor;
self.automaticallyAdjustsScrollViewInsets = false;
NSArray<NSString *> *stringKeys = @[
@"Tour.Title1",
@"Tour.Title2",
@ -227,6 +226,45 @@ typedef enum {
}
}
- (void)animateIn {
CGPoint logoTargetPosition = _glkView.center;
_glkView.center = CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0);
RMIntroPageView *firstPage = (RMIntroPageView *)[_pageViews firstObject];
CGPoint headerTargetPosition = firstPage.headerLabel.center;
firstPage.headerLabel.center = CGPointMake(headerTargetPosition.x, headerTargetPosition.y + 140.0);
CGPoint descriptionTargetPosition = firstPage.descriptionLabel.center;
firstPage.descriptionLabel.center = CGPointMake(descriptionTargetPosition.x, descriptionTargetPosition.y + 160.0);
CGPoint pageControlTargetPosition = _pageControl.center;
_pageControl.center = CGPointMake(pageControlTargetPosition.x, pageControlTargetPosition.y + 200.0);
CGPoint buttonTargetPosition = _startButton.center;
_startButton.center = CGPointMake(buttonTargetPosition.x, buttonTargetPosition.y + 220.0);
_glkView.transform = CGAffineTransformMakeScale(0.68, 0.68);
[UIView animateWithDuration:0.65 delay:0.15 usingSpringWithDamping:1.2f initialSpringVelocity:0.0 options:kNilOptions animations:^{
_glkView.center = logoTargetPosition;
firstPage.headerLabel.center = headerTargetPosition;
firstPage.descriptionLabel.center = descriptionTargetPosition;
_pageControl.center = pageControlTargetPosition;
_startButton.center = buttonTargetPosition;
_glkView.transform = CGAffineTransformIdentity;
} completion:nil];
_pageScrollView.alpha = 0.0;
_pageControl.alpha = 0.0;
_startButton.alpha = 0.0;
[UIView animateWithDuration:0.3 delay:0.15 options:kNilOptions animations:^{
_pageScrollView.alpha = 1.0;
_pageControl.alpha = 1.0;
_startButton.alpha = 1.0;
} completion:nil];
}
- (void)loadGL
{
#if TARGET_OS_SIMULATOR && defined(__aarch64__)
@ -313,6 +351,7 @@ typedef enum {
[self.view addSubview:_wrapperView];
_pageScrollView = [[UIScrollView alloc]initWithFrame:self.view.bounds];
_pageScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
_pageScrollView.clipsToBounds = true;
_pageScrollView.opaque = true;
_pageScrollView.clearsContextBeforeDrawing = false;
@ -512,6 +551,7 @@ typedef enum {
CGFloat startButtonWidth = MIN(430.0 - 48.0, self.view.bounds.size.width - 48.0f);
UIView *startButton = self.createStartButton(startButtonWidth);
if (startButton.superview == nil) {
_startButton = startButton;
[self.view addSubview:startButton];
}
startButton.frame = CGRectMake(floor((self.view.bounds.size.width - startButtonWidth) / 2.0f), self.view.bounds.size.height - startButtonY - statusBarHeight, startButtonWidth, 50.0f);
@ -523,10 +563,9 @@ typedef enum {
_pageScrollView.contentSize=CGSizeMake(_headlines.count * self.view.bounds.size.width, 150);
_pageScrollView.contentOffset = CGPointMake(_currentPage * self.view.bounds.size.width, 0);
[_pageViews enumerateObjectsUsingBlock:^(UIView *pageView, NSUInteger index, __unused BOOL *stop)
{
pageView.frame = CGRectMake(index * self.view.bounds.size.width, (pageY - statusBarHeight), self.view.bounds.size.width, 150);
}];
[_pageViews enumerateObjectsUsingBlock:^(UIView *pageView, NSUInteger index, __unused BOOL *stop) {
pageView.frame = CGRectMake(index * self.view.bounds.size.width, (pageY - statusBarHeight), self.view.bounds.size.width, 150);
}];
}
- (void)viewWillAppear:(BOOL)animated

View File

@ -512,7 +512,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, _ in
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ViewInChannel, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak controller] c, _ in
c.dismiss(completion: {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in

View File

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

View File

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

View File

@ -35,6 +35,7 @@ import BackgroundTasks
import UIKitRuntimeUtils
import StoreKit
import PhoneNumberFormat
import AuthorizationUI
#if canImport(AppCenter)
import AppCenter
@ -284,7 +285,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
private var alertActions: (primary: (() -> Void)?, other: (() -> Void)?)?
private let deviceToken = Promise<Data?>(nil)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
precondition(!testIsLaunched)
testIsLaunched = true
@ -315,6 +316,11 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
self.window = 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
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
@ -1044,7 +1050,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
let startTime = CFAbsoluteTimeGetCurrent()
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")
var network: Network?
@ -1075,8 +1081,15 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
print("Launch to ready took \((CFAbsoluteTimeGetCurrent() - launchStartTime) * 1000.0) ms")
self.mainWindow.debugAction = nil
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 {
let layer = context.rootController.view.layer
layer.allowsGroupOpacity = true
@ -1123,7 +1136,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
let authContextReadyDisposable = MetaDisposable()
self.authContextDisposable.set((self.authContext.get()
|> deliverOnMainQueue).start(next: { context in
|> deliverOnMainQueue).start(next: { [weak launchIconView] context in
var network: Network?
if let context = context {
network = context.account.network
@ -1155,15 +1168,38 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
self.authContextValue = context
if let context = context {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self.mainWindow.present(statusController, on: .root)
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self?.mainWindow.present(statusController, on: .root)
return ActionDisposable { [weak statusController] in
Queue.mainQueue().async() {
statusController?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.5, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let isReady: Signal<Bool, NoError> = context.isReady.get()
authContextReadyDisposable.set((isReady
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
statusController.dismiss()
progressDisposable.dispose()
self.mainWindow.present(context.rootController, on: .root)
if let launchIconView {
if context.rootController.topViewController is AuthorizationSequenceSplashController {
context.rootController.view.addSubview(launchIconView)
Queue.mainQueue().after(0.01, {
launchIconView.removeFromSuperview()
})
} else {
launchIconView.removeFromSuperview()
}
}
}))
} else {
authContextReadyDisposable.set(nil)

View File

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

View File

@ -4131,13 +4131,53 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .join(_, joinHash):
self.controllerInteraction?.openJoinLink(joinHash)
}
}, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId in
guard let self else {
return
}
let context = self.context
let peerId = self.chatLocation.peerId
var createNewGroupImpl: (() -> Void)?
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent], requestPeerType: peerType, hasContactSelector: false, createNewGroup: {
createNewGroupImpl?()
}))
controller.peerSelected = { [weak controller] peer, _ in
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peer.id).start()
controller?.dismiss()
}
createNewGroupImpl = { [weak controller] in
switch peerType {
case .user:
break
case let .group(group):
let createGroupController = createGroupControllerImpl(context: context, peerIds: peerId.flatMap { [$0] } ?? [], mode: .requestPeer(group), completion: { peerId, dismiss in
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peerId).start()
dismiss()
})
createGroupController.navigationPresentation = .modal
controller?.replace(with: createGroupController)
case let .channel(channel):
let createChannelController = createChannelController(context: context, mode: .requestPeer(channel), completion: { peerId, dismiss in
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peerId).start()
dismiss()
})
createChannelController.navigationPresentation = .modal
controller?.replace(with: createChannelController)
}
}
self.push(controller)
}, requestMessageUpdate: { [weak self] id, scroll in
if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)
if let self {
self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)
}
}, cancelInteractiveKeyboardGestures: { [weak self] in
(self?.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
self?.chatDisplayNode.cancelInteractiveKeyboardGestures()
if let self {
(self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures()
self.chatDisplayNode.cancelInteractiveKeyboardGestures()
}
}, dismissTextInput: { [weak self] in
self?.chatDisplayNode.dismissTextInput()
}, scrollToMessageId: { [weak self] index in

View File

@ -1552,7 +1552,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
if isReplyThreadHead {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.SharedMedia_ViewInChat, icon: { theme in
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ViewInChannel, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
c.dismiss(completion: {

View File

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

View File

@ -206,7 +206,13 @@ private func CreateChannelEntries(presentationData: PresentationData, state: Cre
return entries
}
public func createChannelController(context: AccountContext) -> ViewController {
public enum CreateChannelMode {
case generic
case requestPeer(ReplyMarkupButtonRequestPeerType.Channel)
}
public func createChannelController(context: AccountContext, mode: CreateChannelMode = .generic, completion: ((PeerId, @escaping () -> Void) -> Void)? = nil) -> ViewController {
let initialState = CreateChannelState(creating: false, editingName: ItemListAvatarAndNameInfoItemName.title(title: "", type: .channel), editingDescriptionText: "", avatar: nil)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)

View File

@ -29,6 +29,7 @@ import LegacyMediaPickerUI
import ContextUI
import ChatTimerScreen
import AsyncDisplayKit
import TextFormat
private struct CreateGroupArguments {
let context: AccountContext
@ -39,10 +40,14 @@ private struct CreateGroupArguments {
let changeLocation: () -> Void
let updateWithVenue: (TelegramMediaMap) -> Void
let updateAutoDelete: () -> Void
let updatePublicLinkText: (String) -> Void
let openAuction: (String) -> Void
}
private enum CreateGroupSection: Int32 {
case info
case username
case topics
case autoDelete
case members
case location
@ -65,6 +70,12 @@ private enum CreateGroupEntryTag: ItemListItemTag {
private enum CreateGroupEntry: ItemListNodeEntry {
case groupInfo(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?)
case setProfilePhoto(PresentationTheme, String)
case usernameHeader(PresentationTheme, String)
case username(PresentationTheme, String, String)
case usernameStatus(PresentationTheme, String, AddressNameValidationStatus, String, String)
case usernameInfo(PresentationTheme, String)
case topics(PresentationTheme, String)
case topicsInfo(PresentationTheme, String)
case autoDelete(title: String, value: String)
case autoDeleteInfo(String)
case member(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, PeerPresence?)
@ -79,6 +90,10 @@ private enum CreateGroupEntry: ItemListNodeEntry {
switch self {
case .groupInfo, .setProfilePhoto:
return CreateGroupSection.info.rawValue
case .usernameHeader, .username, .usernameStatus, .usernameInfo:
return CreateGroupSection.username.rawValue
case .topics, .topicsInfo:
return CreateGroupSection.topics.rawValue
case .autoDelete, .autoDeleteInfo:
return CreateGroupSection.autoDelete.rawValue
case .member:
@ -96,12 +111,24 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return 0
case .setProfilePhoto:
return 1
case .autoDelete:
case .usernameHeader:
return 2
case .autoDeleteInfo:
case .username:
return 3
case .usernameStatus:
return 4
case .usernameInfo:
return 5
case .topics:
return 6
case .topicsInfo:
return 7
case .autoDelete:
return 8
case .autoDeleteInfo:
return 9
case let .member(index, _, _, _, _, _, _):
return 4 + index
return 10 + index
case .locationHeader:
return 10000
case .location:
@ -153,6 +180,43 @@ private enum CreateGroupEntry: ItemListNodeEntry {
} else {
return false
}
case let .usernameHeader(lhsTheme, lhsText):
if case let .usernameHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .username(lhsTheme, lhsText, lhsValue):
if case let .username(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .usernameStatus(lhsTheme, lhsAddressName, lhsStatus, lhsText, lhsUsername):
if case let .usernameStatus(rhsTheme, rhsAddressName, rhsStatus, rhsText, rhsUsername) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsStatus == rhsStatus, lhsText == rhsText, lhsUsername == rhsUsername {
return true
} else {
return false
}
case let .usernameInfo(lhsTheme, lhsText):
if case let .usernameInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .topics(lhsTheme, lhsText):
if case let .topics(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .topicsInfo(lhsTheme, lhsText):
if case let .topicsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .autoDelete(title, value):
if case .autoDelete(title, value) = rhs {
return true
@ -263,6 +327,41 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.changeProfilePhoto()
})
case let .usernameHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .username(theme, placeholder, text):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in
arguments.updatePublicLinkText(updatedText)
}, action: {
})
case let .usernameStatus(_, _, status, text, username):
var displayActivity = false
let textColor: ItemListActivityTextItem.TextColor
switch status {
case .invalidFormat:
textColor = .destructive
case let .availability(availability):
switch availability {
case .available:
textColor = .constructive
case .purchaseAvailable:
textColor = .generic
case .invalid, .taken:
textColor = .destructive
}
case .checking:
textColor = .generic
displayActivity = true
}
return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: text, color: textColor, linkAction: { _ in
arguments.openAuction(username)
}, sectionId: self.section)
case let .usernameInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .topics(_, text):
return ItemListSwitchItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Topics")?.precomposed(), title: text, value: true, enabled: false, sectionId: self.section, style: .blocks, updated: { _ in })
case let .topicsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .autoDelete(text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
arguments.updateAutoDelete()
@ -299,9 +398,11 @@ private struct CreateGroupState: Equatable {
var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?
var location: PeerGeoLocation?
var autoremoveTimeout: Int32?
var editingPublicLinkText: String?
var addressNameValidationStatus: AddressNameValidationStatus?
}
private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView, venues: [TelegramMediaMap]?, globalAutoremoveTimeout: Int32) -> [CreateGroupEntry] {
private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView, venues: [TelegramMediaMap]?, globalAutoremoveTimeout: Int32, requestPeer: ReplyMarkupButtonRequestPeerType.Group?) -> [CreateGroupEntry] {
var entries: [CreateGroupEntry] = []
let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil)
@ -310,15 +411,68 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat
entries.append(.groupInfo(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, groupInfoState, state.avatar))
let autoremoveTimeout = state.autoremoveTimeout ?? globalAutoremoveTimeout
let autoRemoveText: String
if autoremoveTimeout == 0 {
autoRemoveText = presentationData.strings.Autoremove_OptionOff
if let requestPeer {
if let hasUsername = requestPeer.hasUsername, hasUsername {
let currentUsername = state.editingPublicLinkText ?? ""
entries.append(.usernameHeader(presentationData.theme, presentationData.strings.CreateGroup_PublicLinkTitle.uppercased()))
entries.append(.username(presentationData.theme, "link", currentUsername))
if let status = state.addressNameValidationStatus {
let statusText: String
switch status {
case let .invalidFormat(error):
switch error {
case .startsWithDigit:
statusText = presentationData.strings.Username_InvalidStartsWithNumber
case .startsWithUnderscore:
statusText = presentationData.strings.Username_InvalidStartsWithUnderscore
case .endsWithUnderscore:
statusText = presentationData.strings.Username_InvalidEndsWithUnderscore
case .invalidCharacters:
statusText = presentationData.strings.Username_InvalidCharacters
case .tooShort:
statusText = presentationData.strings.Username_InvalidTooShort
}
case let .availability(availability):
switch availability {
case .available:
statusText = presentationData.strings.Username_UsernameIsAvailable(currentUsername).string
case .invalid:
statusText = presentationData.strings.Username_InvalidCharacters
case .taken:
statusText = presentationData.strings.Username_InvalidTaken
case .purchaseAvailable:
var markdownString = presentationData.strings.Username_UsernamePurchaseAvailable
let entities = generateTextEntities(markdownString, enabledTypes: [.mention])
if let entity = entities.first {
markdownString.insert(contentsOf: "]()", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.upperBound))
markdownString.insert(contentsOf: "[", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.lowerBound))
}
statusText = markdownString
}
case .checking:
statusText = presentationData.strings.Username_CheckingUsername
}
entries.append(.usernameStatus(presentationData.theme, currentUsername, status, statusText, currentUsername))
}
entries.append(.usernameInfo(presentationData.theme, presentationData.strings.CreateGroup_PublicLinkInfo))
}
if let isForum = requestPeer.isForum, isForum {
entries.append(.topics(presentationData.theme, presentationData.strings.PeerInfo_OptionTopics))
entries.append(.topicsInfo(presentationData.theme, presentationData.strings.PeerInfo_OptionTopicsText))
}
} else {
autoRemoveText = timeIntervalString(strings: presentationData.strings, value: autoremoveTimeout)
let autoremoveTimeout = state.autoremoveTimeout ?? globalAutoremoveTimeout
let autoRemoveText: String
if autoremoveTimeout == 0 {
autoRemoveText = presentationData.strings.Autoremove_OptionOff
} else {
autoRemoveText = timeIntervalString(strings: presentationData.strings, value: autoremoveTimeout)
}
entries.append(.autoDelete(title: presentationData.strings.CreateGroup_AutoDeleteTitle, value: autoRemoveText))
entries.append(.autoDeleteInfo(presentationData.strings.CreateGroup_AutoDeleteText))
}
entries.append(.autoDelete(title: presentationData.strings.CreateGroup_AutoDeleteTitle, value: autoRemoveText))
entries.append(.autoDeleteInfo(presentationData.strings.CreateGroup_AutoDeleteText))
var peers: [Peer] = []
for peerId in peerIds {
@ -382,7 +536,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
location = PeerGeoLocation(latitude: latitude, longitude: longitude, address: address ?? "")
}
let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), nameSetFromVenue: false, avatar: nil, location: location, autoremoveTimeout: nil)
let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), nameSetFromVenue: false, avatar: nil, location: location, autoremoveTimeout: nil, editingPublicLinkText: nil, addressNameValidationStatus: nil)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in
@ -400,6 +554,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
let actionsDisposable = DisposableSet()
let checkAddressNameDisposable = MetaDisposable()
actionsDisposable.add(checkAddressNameDisposable)
let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
let uploadedAvatar = Promise<UploadedPeerPhotoData>()
@ -429,13 +586,13 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
return current
}
}, done: {
let (creating, title, location) = stateValue.with { state -> (Bool, String, PeerGeoLocation?) in
return (state.creating, state.editingName.composedTitle, state.location)
let (creating, title, location, publicLink) = stateValue.with { state -> (Bool, String, PeerGeoLocation?, String?) in
return (state.creating, state.editingName.composedTitle, state.location, state.editingPublicLinkText)
}
if !creating && !title.isEmpty {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.GlobalAutoremoveTimeout())
|> deliverOnMainQueue).start(next: { maybeGlobalAutoremoveTimeout in
|> deliverOnMainQueue).start(next: { maybeGlobalAutoremoveTimeout in
updateState { current in
var current = current
current.creating = true
@ -447,7 +604,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
let autoremoveTimeout = stateValue.with({ $0 }).autoremoveTimeout ?? globalAutoremoveTimeout
let ttlPeriod: Int32? = autoremoveTimeout == 0 ? nil : autoremoveTimeout
let createSignal: Signal<PeerId?, CreateGroupError>
var createSignal: Signal<PeerId?, CreateGroupError>
switch mode {
case .generic:
createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds, ttlPeriod: ttlPeriod)
@ -496,10 +653,85 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
}
}
case let .requestPeer(group):
var isForum = false
if let isForumRequested = group.isForum, isForumRequested {
isForum = true
}
if isForum {
createSignal = context.engine.peers.createSupergroup(title: title, description: nil, isForum: true)
|> map(Optional.init)
|> mapError { error -> CreateGroupError in
switch error {
case .generic:
return .generic
case .restricted:
return .restricted
case .tooMuchJoined:
return .tooMuchJoined
case .tooMuchLocationBasedGroups:
return .tooMuchLocationBasedGroups
case let .serverProvided(error):
return .serverProvided(error)
}
}
if let publicLink, !publicLink.isEmpty {
createSignal = createSignal
|> mapToSignal { peerId in
if let peerId = peerId {
return context.engine.peers.updateAddressName(domain: .peer(peerId), name: publicLink)
|> mapError { _ in
return .generic
}
|> map { _ in
return peerId
}
} else {
return .fail(.generic)
}
}
}
} else {
if let publicLink, !publicLink.isEmpty {
createSignal = context.engine.peers.createSupergroup(title: title, description: nil)
|> map(Optional.init)
|> mapError { error -> CreateGroupError in
switch error {
case .generic:
return .generic
case .restricted:
return .restricted
case .tooMuchJoined:
return .tooMuchJoined
case .tooMuchLocationBasedGroups:
return .tooMuchLocationBasedGroups
case let .serverProvided(error):
return .serverProvided(error)
}
}
|> mapToSignal { peerId in
if let peerId = peerId {
return context.engine.peers.updateAddressName(domain: .peer(peerId), name: publicLink)
|> mapError { _ in
return .generic
}
|> map { _ in
return peerId
}
} else {
return .fail(.generic)
}
}
} else {
createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds, ttlPeriod: nil)
}
}
}
actionsDisposable.add((createSignal
|> mapToSignal { peerId -> Signal<PeerId?, CreateGroupError> in
|> mapToSignal { peerId -> Signal<PeerId?, CreateGroupError> in
guard let peerId = peerId else {
return .single(nil)
}
@ -910,8 +1142,42 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
presentInGlobalOverlay?(contextController)
}
})
}, updatePublicLinkText: { text in
if text.isEmpty {
checkAddressNameDisposable.set(nil)
updateState { state in
var updated = state
updated.editingPublicLinkText = text
updated.addressNameValidationStatus = nil
return updated
}
} else {
updateState { state in
var updated = state
updated.editingPublicLinkText = text
return updated
}
checkAddressNameDisposable.set((context.engine.peers.validateAddressNameInteractive(domain: .peer(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(0))), name: text)
|> deliverOnMainQueue).start(next: { result in
updateState { state in
var updated = state
updated.addressNameValidationStatus = result
return updated
}
}))
}
}, openAuction: { username in
endEditingImpl?()
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/username/\(username)", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
})
var requestPeer: ReplyMarkupButtonRequestPeerType.Group?
if case let .requestPeer(peerType) = mode {
requestPeer = peerType
}
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get(),
@ -921,7 +1187,6 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.GlobalAutoremoveTimeout())
)
|> map { presentationData, state, view, address, venues, globalAutoremoveTimeout -> (ItemListControllerState, (ItemListNodeState, Any)) in
let rightNavigationButton: ItemListNavigationButton
if state.creating {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
@ -932,7 +1197,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Compose_NewGroupTitle), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view, venues: venues, globalAutoremoveTimeout: globalAutoremoveTimeout ?? 0), style: .blocks, focusItemTag: CreateGroupEntryTag.info)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view, venues: venues, globalAutoremoveTimeout: globalAutoremoveTimeout ?? 0, requestPeer: requestPeer), style: .blocks, focusItemTag: CreateGroupEntryTag.info)
return (controllerState, (listState, arguments))
}
@ -941,6 +1206,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
let controller = ItemListController(context: context, state: signal)
controller.beganInteractiveDragging = {
endEditingImpl?()
}
replaceControllerImpl = { [weak controller] value in
(controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true)
}

View File

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

View File

@ -2728,6 +2728,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _ in
}, openRequestedPeerSelection: { _, _, _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {
@ -3093,8 +3094,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
return
}
}
if peer.firstName != firstName || peer.lastName != lastName || (bio != nil && bio != cachedData.about) {
if (peer.firstName ?? "") != firstName || (peer.lastName ?? "") != lastName || (bio ?? "") != (cachedData.about ?? "") {
var updateNameSignal: Signal<Void, NoError> = .complete()
var hasProgress = false
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 forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
override public var _presentedInModal: Bool {
get {
@ -93,12 +94,29 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
self.forwardedMessageIds = params.forwardedMessageIds
self.hasTypeHeaders = params.hasTypeHeaders
self.selectForumThreads = params.selectForumThreads
self.requestPeerType = params.requestPeerType
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.customTitle = params.title
if let peerType = params.requestPeerType {
switch peerType {
case let .user(user):
if let isBot = user.isBot, isBot {
self.customTitle = self.presentationData.strings.RequestPeer_ChooseBotTitle
} else {
self.customTitle = self.presentationData.strings.RequestPeer_ChooseUserTitle
}
case .group:
self.customTitle = self.presentationData.strings.RequestPeer_ChooseGroupTitle
case .channel:
self.customTitle = self.presentationData.strings.RequestPeer_ChooseChannelTitle
}
}
self.title = self.customTitle ?? self.presentationData.strings.Conversation_ForwardTitle
if params.forumPeerId == nil {
@ -159,7 +177,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
}
override public func loadDisplayNode() {
self.displayNode = PeerSelectionControllerNode(context: self.context, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, createNewGroup: self.createNewGroup, present: { [weak self] c, a in
self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, createNewGroup: self.createNewGroup, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, presentInGlobalOverlay: { [weak self] c, a in
self?.presentInGlobalOverlay(c, with: a)

View File

@ -18,9 +18,13 @@ import ChatSendMessageActionUI
import ChatTextLinkEditUI
import AnimationCache
import MultiAnimationRenderer
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
final class PeerSelectionControllerNode: ASDisplayNode {
private let context: AccountContext
private weak var controller: PeerSelectionController?
private let present: (ViewController, Any?) -> Void
private let presentInGlobalOverlay: (ViewController, Any?) -> Void
private let dismiss: () -> Void
@ -29,6 +33,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
private let hasGlobalSearch: Bool
private let forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private var presentationInterfaceState: ChatPresentationInterfaceState
private var interfaceInteraction: ChatPanelInterfaceInteraction?
@ -41,6 +46,16 @@ final class PeerSelectionControllerNode: ASDisplayNode {
var navigationBar: NavigationBar?
private let requirementsBackgroundNode: NavigationBackgroundNode?
private let requirementsSeparatorNode: ASDisplayNode?
private let requirementsTextNode: ImmediateTextNode?
private let emptyAnimationNode: AnimatedStickerNode
private var emptyAnimationSize = CGSize()
private let emptyTitleNode: ImmediateTextNode
private let emptyTextNode: ImmediateTextNode
private let emptyButtonNode: SolidRoundedButtonNode
private let toolbarBackgroundNode: NavigationBackgroundNode?
private let toolbarSeparatorNode: ASDisplayNode?
private let segmentedControlNode: SegmentedControlNode?
@ -83,12 +98,15 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return self.readyValue.get()
}
private var isEmpty = false
private var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) {
return (self.presentationData, self.presentationDataPromise.get())
}
init(context: AccountContext, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
init(context: AccountContext, controller: PeerSelectionController, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: ReplyMarkupButtonRequestPeerType?, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
self.context = context
self.controller = controller
self.present = present
self.presentInGlobalOverlay = presentInGlobalOverlay
self.dismiss = dismiss
@ -97,6 +115,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.hasGlobalSearch = hasGlobalSearch
self.forwardedMessageIds = forwardedMessageIds
self.hasTypeHeaders = hasTypeHeaders
self.requestPeerType = requestPeerType
self.presentationData = presentationData
@ -107,6 +126,43 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) }
if let _ = self.requestPeerType {
self.requirementsBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
self.requirementsSeparatorNode = ASDisplayNode()
self.requirementsSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.requirementsTextNode = ImmediateTextNode()
self.requirementsTextNode?.maximumNumberOfLines = 0
self.requirementsTextNode?.lineSpacing = 0.1
} else {
self.requirementsBackgroundNode = nil
self.requirementsSeparatorNode = nil
self.requirementsTextNode = nil
}
self.emptyTitleNode = ImmediateTextNode()
self.emptyTitleNode.displaysAsynchronously = false
self.emptyTitleNode.maximumNumberOfLines = 0
self.emptyTitleNode.isHidden = true
self.emptyTitleNode.textAlignment = .center
self.emptyTitleNode.lineSpacing = 0.25
self.emptyTextNode = ImmediateTextNode()
self.emptyTextNode.displaysAsynchronously = false
self.emptyTextNode.maximumNumberOfLines = 0
self.emptyTextNode.isHidden = true
self.emptyTextNode.lineSpacing = 0.25
self.emptyAnimationNode = DefaultAnimatedStickerNodeImpl()
self.emptyAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListNoResults"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.emptyAnimationNode.isHidden = true
self.emptyAnimationSize = CGSize(width: 120.0, height: 120.0)
self.emptyButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), cornerRadius: 11.0, gloss: true)
self.emptyButtonNode.isHidden = true
self.emptyButtonNode.pressed = {
createNewGroup?()
}
if hasChatListSelector && hasContactSelector {
self.toolbarBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
@ -136,8 +192,15 @@ final class PeerSelectionControllerNode: ASDisplayNode {
} else {
chatListLocation = .chatList(groupId: .root)
}
let chatListMode: ChatListNodeMode
if let requestPeerType = self.requestPeerType {
chatListMode = .peerType(type: requestPeerType)
} else {
chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false)
}
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
super.init()
@ -184,6 +247,17 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return self?.contentScrollingEnded?(listView) ?? false
}
self.chatListNode.isEmptyUpdated = { [weak self] state, _, _ in
guard let strongSelf = self else {
return
}
if case .empty(false, _) = state, let (layout, navigationBarHeight, actualNavigationBarHeight) = strongSelf.containerLayout {
strongSelf.isEmpty = true
strongSelf.controller?.navigationBar?.setContentNode(nil, animated: false)
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate)
}
}
self.addSubnode(self.chatListNode)
if hasChatListSelector && hasContactSelector {
@ -196,6 +270,17 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.addSubnode(self.segmentedControlNode!)
}
if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode {
self.chatListNode.addSubnode(requirementsBackgroundNode)
self.chatListNode.addSubnode(requirementsSeparatorNode)
self.chatListNode.addSubnode(requirementsTextNode)
self.addSubnode(self.emptyAnimationNode)
self.addSubnode(self.emptyTitleNode)
self.addSubnode(self.emptyTextNode)
self.addSubnode(self.emptyButtonNode)
}
if !hasChatListSelector && hasContactSelector {
self.indexChanged(1)
}
@ -484,6 +569,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.updateChatPresentationInterfaceState({ $0.updatedTheme(self.presentationData.theme) })
self.requirementsBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.toolbarBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.segmentedControlNode?.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme))
@ -574,6 +660,113 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
if let requestPeerType = self.requestPeerType {
if self.isEmpty {
self.chatListNode.isHidden = true
self.requirementsBackgroundNode?.isHidden = true
self.requirementsTextNode?.isHidden = true
self.requirementsSeparatorNode?.isHidden = true
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
var emptyTitle: String
var emptyText: String
var emptyButtonText: String
switch requestPeerType {
case let .user(user):
if let isBot = user.isBot, isBot {
emptyTitle = self.presentationData.strings.RequestPeer_BotsAllEmpty
emptyText = ""
} else {
emptyTitle = self.presentationData.strings.RequestPeer_UsersAllEmpty
if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) {
emptyTitle = self.presentationData.strings.RequestPeer_UsersEmpty
emptyText = text
} else {
emptyText = ""
}
}
emptyButtonText = ""
case .group:
emptyTitle = self.presentationData.strings.RequestPeer_GroupsAllEmpty
if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) {
emptyTitle = self.presentationData.strings.RequestPeer_GroupsEmpty
emptyText = text
} else {
emptyText = ""
}
emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup
case .channel:
emptyTitle = self.presentationData.strings.RequestPeer_ChannelsEmpty
if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) {
emptyTitle = self.presentationData.strings.RequestPeer_ChannelsEmpty
emptyText = text
} else {
emptyText = ""
}
emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup
}
self.emptyTitleNode.attributedText = NSAttributedString(string: emptyTitle, font: Font.semibold(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: Font.regular(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
let padding: CGFloat = 44.0
let emptyTitleSize = self.emptyTitleNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyAnimationHeight = self.emptyAnimationSize.height
let emptyAnimationSpacing: CGFloat = 12.0
let emptyTextSpacing: CGFloat = 17.0
var emptyButtonSpacing: CGFloat = 15.0
var emptyButtonHeight: CGFloat = 50.0
if emptyButtonText.isEmpty {
emptyButtonSpacing = 0.0
emptyButtonHeight = 0.0
}
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + emptyButtonSpacing + emptyButtonHeight
let emptyAnimationY = floorToScreenPixels((layout.size.height - emptyTotalHeight) / 2.0)
if !emptyButtonText.isEmpty {
let buttonPadding: CGFloat = 30.0
self.emptyButtonNode.title = emptyButtonText
self.emptyButtonNode.isHidden = false
let emptyButtonWidth = layout.size.width - insets.left - insets.right - buttonPadding * 2.0
let _ = self.emptyButtonNode.updateLayout(width: emptyButtonWidth, transition: transition)
transition.updateFrame(node: self.emptyButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyButtonWidth) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing + emptyTextSize.height + emptyButtonSpacing), size: CGSize(width: emptyButtonWidth, height: emptyButtonHeight)))
} else {
self.emptyButtonNode.isHidden = true
}
let textTransition = ContainedViewLayoutTransition.immediate
textTransition.updateFrame(node: self.emptyAnimationNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - self.emptyAnimationSize.width) / 2.0), y: emptyAnimationY), size: self.emptyAnimationSize))
textTransition.updateFrame(node: self.emptyTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTitleSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize))
textTransition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTextSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize))
self.emptyAnimationNode.updateLayout(size: self.emptyAnimationSize)
self.emptyAnimationNode.isHidden = false
self.emptyTitleNode.isHidden = false
self.emptyTextNode.isHidden = false
self.emptyAnimationNode.visibility = true
} else if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode, let requirementsText = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: true) {
let requirements = NSMutableAttributedString(string: self.presentationData.strings.RequestPeer_Requirements + "\n", font: Font.semibold(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
requirements.append(NSAttributedString(string: requirementsText, font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor))
requirementsTextNode.attributedText = requirements
let sideInset: CGFloat = 16.0
let verticalInset: CGFloat = 11.0
let requirementsSize = requirementsTextNode.updateLayout(CGSize(width: layout.size.width - insets.left - insets.right - sideInset * 2.0, height: .greatestFiniteMagnitude))
let requirementsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: actualNavigationBarHeight), size: CGSize(width: layout.size.width, height: requirementsSize.height + verticalInset * 2.0))
insets.top += requirementsBackgroundFrame.height
requirementsBackgroundNode.update(size: requirementsBackgroundFrame.size, transition: transition)
transition.updateFrame(node: requirementsBackgroundNode, frame: requirementsBackgroundFrame)
transition.updateFrame(node: requirementsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: requirementsBackgroundFrame.maxY - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
requirementsTextNode.frame = CGRect(origin: CGPoint(x: insets.left + sideInset, y: requirementsBackgroundFrame.minY + verticalInset), size: requirementsSize)
}
}
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve)
@ -614,6 +807,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
animationRenderer: self.animationRenderer,
updatedPresentationData: self.updatedPresentationData,
filter: self.filter,
requestPeerType: self.requestPeerType,
location: chatListLocation,
displaySearchFilters: false,
hasDownloads: false,
@ -865,3 +1059,100 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}
}
}
private func stringForRequestPeerType(strings: PresentationStrings, peerType: ReplyMarkupButtonRequestPeerType, offset: Bool) -> String? {
var lines: [String] = []
func append(_ string: String) {
if offset {
lines.append("\(string)")
} else {
lines.append("\(string)")
}
}
switch peerType {
case let .user(user):
if let isPremium = user.isPremium {
if isPremium {
append(strings.RequestPeer_Requirement_UserPremiumOn)
} else {
append(strings.RequestPeer_Requirement_UserPremiumOff)
}
}
case let .group(group):
if group.isCreator {
append(strings.RequestPeer_Requirement_Group_CreatorOn)
}
if let hasUsername = group.hasUsername {
if hasUsername {
append(strings.RequestPeer_Requirement_Group_HasUsernameOn)
} else {
append(strings.RequestPeer_Requirement_Group_HasUsernameOff)
}
}
if let isForum = group.isForum {
if isForum {
append(strings.RequestPeer_Requirement_Group_ForumOn)
} else {
append(strings.RequestPeer_Requirement_Group_ForumOff)
}
}
if let adminRights = group.userAdminRights {
var rights: [String] = []
if adminRights.rights.contains(.canChangeInfo) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Info)
}
if adminRights.rights.contains(.canPostMessages) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Send)
}
if adminRights.rights.contains(.canDeleteMessages) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Delete)
}
if adminRights.rights.contains(.canEditMessages) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Edit)
}
if adminRights.rights.contains(.canBanUsers) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Ban)
}
if adminRights.rights.contains(.canInviteUsers) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Invite)
}
if adminRights.rights.contains(.canPinMessages) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Pin)
}
if adminRights.rights.contains(.canManageTopics) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Topics)
}
if adminRights.rights.contains(.canManageCalls) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_VideoChats)
}
if adminRights.rights.contains(.canBeAnonymous) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Anonymous)
}
if adminRights.rights.contains(.canAddAdmins) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_AddAdmins)
}
if !rights.isEmpty {
let rightsString = strings.RequestPeer_Requirement_Group_Rights(String(rights.joined(separator: ", "))).string
append(rightsString)
}
}
case let .channel(channel):
if channel.isCreator {
append(strings.RequestPeer_Requirement_Channel_CreatorOn)
}
if let hasUsername = channel.hasUsername {
if hasUsername {
append(strings.RequestPeer_Requirement_Channel_HasUsernameOn)
} else {
append(strings.RequestPeer_Requirement_Channel_HasUsernameOff)
}
}
}
if lines.isEmpty {
return nil
} else {
return String(lines.joined(separator: "\n"))
}
}

View File

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

View File

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

View File

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