mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Bot peer request support
Launch screen icon
This commit is contained in:
parent
58e641230a
commit
930e237bf1
@ -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>
|
||||
|
@ -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";
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -15,6 +15,7 @@ swift_library(
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -66,6 +66,8 @@
|
||||
- (UIView *)createAnimationSnapshot;
|
||||
- (UIView *)createTextSnapshot;
|
||||
|
||||
- (void)animateIn;
|
||||
|
||||
@property (nonatomic) bool isEnabled;
|
||||
|
||||
- (void)startTimer;
|
||||
|
@ -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
|
||||
|
@ -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];
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -555,6 +555,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _ in
|
||||
}, openRequestedPeerSelection: { _, _, _ in
|
||||
}, requestMessageUpdate: { _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -158,6 +158,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _ in
|
||||
}, openRequestedPeerSelection: { _, _, _ in
|
||||
}, requestMessageUpdate: { _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
@ -1379,6 +1379,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _ in
|
||||
}, openRequestedPeerSelection: { _, _, _ in
|
||||
}, requestMessageUpdate: { _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user