Merge commit 'e605e58aa31055e4485df1ab97a97c2f53f8f109'

This commit is contained in:
Ali 2023-01-15 20:42:40 +04:00
commit 1af7d86c0c
45 changed files with 1446 additions and 155 deletions

View File

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

View File

@ -8624,3 +8624,75 @@ 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";
"RequestPeer.SelectionConfirmationText" = "Are you sure you want to send %1$@ to %2$@?";
"RequestPeer.SelectionConfirmationInviteText" = "Are you sure you want to send %1$@ to %2$@? This will also add %3$@ to %4$@.";
"RequestPeer.SelectionConfirmationInviteWithRightsText" = "Are you sure you want to send %1$@ to %2$@? This will also add %3$@ to %4$@ with the following rights: %5$@.";
"RequestPeer.SelectionConfirmationSend" = "Send";
"CreateGroup.PublicLinkTitle" = "SET A PUBLIC LINK";
"CreateGroup.PublicLinkInfo" = "You can use **a-z**, **0-9** and underscores. Minimum length is **5** characters.";
"Notification.RequestedPeer" = "You shared %@ with the bot.";
"Conversation.ViewInChannel" = "View in Channel";

View File

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

View File

@ -613,6 +613,8 @@ public protocol ChatController: ViewController {
func beginMessageSearch(_ query: String)
func displayPromoAnnouncement(text: String)
func updateIsPushed(_ isPushed: Bool)
func hintPlayNextOutgoingGift()
var isSendButtonVisible: Bool { get }

View File

@ -41,6 +41,7 @@ public final class PeerSelectionControllerParams {
public let context: AccountContext
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
public let filter: ChatListNodePeersFilter
public let requestPeerType: ReplyMarkupButtonRequestPeerType?
public let forumPeerId: EnginePeer.Id?
public let hasChatListSelector: Bool
public let hasContactSelector: Bool
@ -54,10 +55,11 @@ public final class PeerSelectionControllerParams {
public let hasTypeHeaders: Bool
public let selectForumThreads: Bool
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false, selectForumThreads: Bool = false) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter = [.onlyWriteable], requestPeerType: ReplyMarkupButtonRequestPeerType? = nil, forumPeerId: EnginePeer.Id? = nil, hasChatListSelector: Bool = true, hasContactSelector: Bool = true, hasGlobalSearch: Bool = true, title: String? = nil, attemptSelection: ((Peer, Int64?) -> Void)? = nil, createNewGroup: (() -> Void)? = nil, pretendPresentedInModal: Bool = false, multipleSelection: Bool = false, forwardedMessageIds: [EngineMessage.Id] = [], hasTypeHeaders: Bool = false, selectForumThreads: Bool = false) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.filter = filter
self.requestPeerType = requestPeerType
self.forumPeerId = forumPeerId
self.hasChatListSelector = hasChatListSelector
self.hasContactSelector = hasContactSelector

View File

@ -1117,8 +1117,11 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
let wasEmpty = self.viewControllers.isEmpty
super.setViewControllers(viewControllers, animated: animated)
if wasEmpty {
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
self._ready.set(.single(true))
@ -1150,7 +1153,13 @@ public final class AuthorizationSequenceController: NavigationController, MFMail
}
private func animateIn() {
if !self.otherAccountPhoneNumbers.1.isEmpty {
self.view.layer.animatePosition(from: CGPoint(x: self.view.layer.position.x, y: self.view.layer.position.y + self.view.layer.bounds.size.height), to: self.view.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
} else {
if let splashController = self.topViewController as? AuthorizationSequenceSplashController {
splashController.animateIn()
}
}
}
private func animateOut(completion: (() -> Void)? = nil) {

View File

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

View File

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

View File

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

View File

@ -86,6 +86,7 @@ private struct ChatListSearchContainerNodeSearchState: Equatable {
public final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
private let context: AccountContext
private let peersFilter: ChatListNodePeersFilter
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private let location: ChatListControllerLocation
private let displaySearchFilters: Bool
private let hasDownloads: Bool
@ -135,7 +136,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private var validLayout: (ContainerViewLayout, CGFloat)?
public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) {
public init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, filter: ChatListNodePeersFilter, requestPeerType: ReplyMarkupButtonRequestPeerType?, location: ChatListControllerLocation, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter = .chats, openPeer originalOpenPeer: @escaping (EnginePeer, EnginePeer?, Int64?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer, Int64?) -> Void, openRecentPeerOptions: @escaping (EnginePeer) -> Void, openMessage originalOpenMessage: @escaping (EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void, addContact: ((String) -> Void)?, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, navigationController: NavigationController?) {
var initialFilter = initialFilter
if case .chats = initialFilter, case .forum = location {
initialFilter = .topics
@ -143,6 +144,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.context = context
self.peersFilter = filter
self.requestPeerType = requestPeerType
self.location = location
self.displaySearchFilters = displaySearchFilters
self.hasDownloads = hasDownloads
@ -160,7 +162,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
self.filterContainerNode = ChatListSearchFiltersContainerNode()
self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController)
self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController)
self.paneContainerNode.clipsToBounds = true
super.init()

View File

@ -933,6 +933,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let animationRenderer: MultiAnimationRenderer
private let interaction: ChatListSearchInteraction
private let peersFilter: ChatListNodePeersFilter
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private var presentationData: PresentationData
private let key: ChatListSearchPaneKey
private let tagMask: EngineMessage.Tags?
@ -981,7 +982,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let emptyResultsTitleNode: ImmediateTextNode
private let emptyResultsTextNode: ImmediateTextNode
private let emptyResultsAnimationNode: AnimatedStickerNode
private var emptyResultsAnimationSize: CGSize = CGSize()
private var emptyResultsAnimationSize = CGSize()
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, presentationData: PresentationData)?
@ -1002,7 +1003,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private var hiddenMediaDisposable: Disposable?
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: ReplyMarkupButtonRequestPeerType?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
self.context = context
self.animationCache = animationCache
self.animationRenderer = animationRenderer
@ -1018,6 +1019,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
peersFilter.insert(.excludeRecent)
}
self.peersFilter = peersFilter
self.requestPeerType = requestPeerType
let tagMask: EngineMessage.Tags?
switch key {
@ -1645,6 +1647,115 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let _ = currentRemotePeers.swap((foundRemotePeers.0, foundRemotePeers.1))
let filteredPeer: (EnginePeer, EnginePeer) -> Bool = { peer, accountPeer in
if let peerType = requestPeerType {
guard !peer.isDeleted else {
return false
}
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 {
if isForum != channel.flags.contains(.isForum) {
return false
}
}
if let hasUsername = groupType.hasUsername {
if hasUsername != (channel.addressName != nil) {
return false
}
}
if let userAdminRights = groupType.userAdminRights {
if channel.flags.contains(.isCreator) {
if let rights = channel.adminRights, rights.rights.contains(.canBeAnonymous) != userAdminRights.rights.contains(.canBeAnonymous) {
return false
}
} 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
}
}
if let userAdminRights = channelType.userAdminRights {
if channel.flags.contains(.isCreator) {
if let rights = channel.adminRights, rights.rights.contains(.canBeAnonymous) != userAdminRights.rights.contains(.canBeAnonymous) {
return false
}
} else if let rights = channel.adminRights {
if rights.rights.intersection(userAdminRights.rights) != userAdminRights.rights {
return false
}
} else {
return false
}
}
return true
} else {
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 }
@ -1674,6 +1785,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
return false
}
}
}
return true
}

View File

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

View File

@ -21,6 +21,7 @@ import Postbox
public enum ChatListNodeMode {
case chatList
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool)
case peerType(type: ReplyMarkupButtonRequestPeerType)
}
struct ChatListNodeListViewTransition {
@ -307,10 +308,13 @@ 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 .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),
context: context,
@ -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,10 +872,13 @@ 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 .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),
context: context,
@ -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,129 @@ public final class ChatListNode: ListView {
}
}
isEmpty = false
return true
case let .peerType(peerType):
if let peer = peer.peer, !peer.isDeleted {
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 {
if isForum != channel.flags.contains(.isForum) {
return false
}
}
if let hasUsername = groupType.hasUsername {
if hasUsername != (channel.addressName != nil) {
return false
}
}
if let userAdminRights = groupType.userAdminRights {
if channel.flags.contains(.isCreator) {
if let rights = channel.adminRights, rights.rights.contains(.canBeAnonymous) != userAdminRights.rights.contains(.canBeAnonymous) {
return false
}
} 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
}
}
if let userAdminRights = channelType.userAdminRights {
if channel.flags.contains(.isCreator) {
if let rights = channel.adminRights, rights.rights.contains(.canBeAnonymous) != userAdminRights.rights.contains(.canBeAnonymous) {
return false
}
} 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
}
}
} 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 +2037,7 @@ public final class ChatListNode: ListView {
switch mode {
case .chatList:
initialLocation = .initial(count: 50, filter: self.chatListFilter)
case .peers:
case .peers, .peerType:
initialLocation = .initial(count: 200, filter: self.chatListFilter)
}
self.setChatListLocation(initialLocation)

View File

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

View File

@ -1309,18 +1309,6 @@ open class NavigationController: UINavigationController, ContainableController,
completion()
}
public func updateContainerPulled(_ pushed: Bool) {
guard self.modalContainers.isEmpty else {
return
}
if let rootContainer = self.rootContainer, case let .flat(container) = rootContainer {
let scale: CGFloat = pushed ? 1.06 : 1.0
container.view.layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
container.view.layer.animateScale(from: pushed ? 1.0 : 1.06, to: scale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
}
}
open override func pushViewController(_ viewController: UIViewController, animated: Bool) {
var controllers = self.viewControllers
controllers.append(viewController)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,6 +102,7 @@ typedef enum {
NSDictionary<NSString *, NSString *> *_englishStrings;
UIView *_wrapperView;
UIView *_startButton;
bool _loadedView;
}
@ -124,8 +125,6 @@ typedef enum {
_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,8 +563,7 @@ 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)
{
[_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);
}];
}

View File

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

View File

@ -6153,11 +6153,9 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
hasPhotos = true
}
let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context)
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)!
mixin.forceDark = true
mixin.stickersContext = paintStickersContext
mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context)
let _ = strongSelf.currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { [weak self] assetsController in
guard let strongSelf = self else {

View File

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

View File

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

View File

@ -35,6 +35,7 @@ import BackgroundTasks
import UIKitRuntimeUtils
import StoreKit
import PhoneNumberFormat
import AuthorizationUI
#if canImport(AppCenter)
import AppCenter
@ -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 progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self.mainWindow.present(statusController, on: .root)
self?.mainWindow.present(statusController, on: .root)
return ActionDisposable { [weak statusController] in
Queue.mainQueue().async() {
statusController?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.5, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let isReady: Signal<Bool, NoError> = context.isReady.get()
authContextReadyDisposable.set((isReady
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { _ in
statusController.dismiss()
progressDisposable.dispose()
self.mainWindow.present(context.rootController, on: .root)
if let launchIconView {
if context.rootController.topViewController is AuthorizationSequenceSplashController {
context.rootController.view.addSubview(launchIconView)
Queue.mainQueue().after(0.01, {
launchIconView.removeFromSuperview()
})
} else {
launchIconView.removeFromSuperview()
}
}
}))
} else {
authContextReadyDisposable.set(nil)

View File

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

View File

@ -4131,13 +4131,79 @@ 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 botName = self.presentationInterfaceState.renderedPeer?.peer.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
let context = self.context
let peerId = self.chatLocation.peerId
var createNewGroupImpl: (() -> Void)?
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: peerType, hasContactSelector: false, createNewGroup: {
createNewGroupImpl?()
}))
controller.peerSelected = { [weak self, weak controller] peer, _ in
guard let strongSelf = self else {
return
}
let peerName = EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)
let text: String
if case .user = peerType {
text = strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationText(peerName, botName).string
} else {
var botAdminRights: TelegramChatAdminRights?
switch peerType {
case let .group(group):
botAdminRights = group.botAdminRights
case let .channel(channel):
botAdminRights = channel.botAdminRights
default:
break
}
if let botAdminRights {
text = strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationInviteWithRightsText(peerName, botName, botName, peerName, stringForAdminRights(strings: strongSelf.presentationData.strings, adminRights: botAdminRights)).string
} else {
text = strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationInviteText(peerName, botName, botName, peerName).string
}
}
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.RequestPeer_SelectionConfirmationSend, action: { [weak controller] in
let _ = context.engine.peers.sendBotRequestedPeer(messageId: messageId, buttonId: buttonId, requestedPeerId: peer.id).start()
controller?.dismiss()
})]), in: .window(.root))
}
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
@ -17536,6 +17602,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.chatDisplayNode.historyNodeContainer.layer.addShakeAnimation(amplitude: -6.0, decay: true)
}
public func updateIsPushed(_ isPushed: Bool) {
let scale: CGFloat = isPushed ? 0.94 : 1.0
let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0))
transition.updateTransformScale(node: self.chatDisplayNode.historyNodeContainer, scale: scale)
}
}
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {

View File

@ -1432,7 +1432,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
transition.updateFrame(node: self.backgroundNode, frame: contentBounds)
self.backgroundNode.updateLayout(size: contentBounds.size, transition: transition)
transition.updateFrame(node: self.historyNodeContainer, frame: contentBounds)
transition.updateBounds(node: self.historyNodeContainer, bounds: contentBounds)
transition.updatePosition(node: self.historyNodeContainer, position: contentBounds.center)
transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size))
transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0))
if let blurredHistoryNode = self.blurredHistoryNode {

View File

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

View File

@ -19,6 +19,7 @@ import TextNodeWithEntities
import AnimationCache
import MultiAnimationRenderer
import ChatControllerInteraction
import ShimmerEffect
private let buttonFont = Font.semibold(13.0)
@ -47,6 +48,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
private let iconNode: ASImageNode
private let highlightedTextNode: TextNode
private let backgroundNode: ASImageNode
private let shimmerEffectNode: ShimmerEffectForegroundNode
private var regularImage: UIImage?
private var highlightedImage: UIImage?
@ -55,12 +57,17 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
var pressed: (() -> Void)?
private var titleColor: UIColor?
init() {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.highlightedTextNode = TextNode()
self.highlightedTextNode.isUserInteractionEnabled = false
self.shimmerEffectNode = ShimmerEffectForegroundNode()
self.shimmerEffectNode.cornerRadius = 5.0
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
@ -73,6 +80,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
super.init()
self.addSubnode(self.shimmerEffectNode)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.addSubnode(self.highlightedTextNode)
@ -109,7 +117,26 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
self.pressed?()
}
static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> (_ width: CGFloat, _ regularImage: UIImage, _ highlightedImage: UIImage, _ iconImage: UIImage?, _ highlightedIconImage: UIImage?, _ title: String, _ titleColor: UIColor, _ highlightedTitleColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode)) {
func startShimmering() {
guard let titleColor = self.titleColor else {
return
}
self.shimmerEffectNode.isHidden = false
self.shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let backgroundFrame = self.backgroundNode.frame
self.shimmerEffectNode.frame = backgroundFrame
self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size)
self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: titleColor.withAlphaComponent(0.3), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
}
func stopShimmering() {
self.shimmerEffectNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.shimmerEffectNode.isHidden = true
})
}
static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> (_ width: CGFloat, _ regularImage: UIImage, _ highlightedImage: UIImage, _ iconImage: UIImage?, _ highlightedIconImage: UIImage?, _ title: String, _ titleColor: UIColor, _ highlightedTitleColor: UIColor, _ inProgress: Bool) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode)) {
let previousRegularImage = current?.regularImage
let previousHighlightedImage = current?.highlightedImage
let previousRegularIconImage = current?.regularIconImage
@ -118,7 +145,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout)
let maybeMakeHighlightedTextLayout = (current?.highlightedTextNode).flatMap(TextNode.asyncLayout)
return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, title, titleColor, highlightedTitleColor in
return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, title, titleColor, highlightedTitleColor, inProgress in
let targetNode: ChatMessageAttachedContentButtonNode
if let current = current {
targetNode = current
@ -175,6 +202,8 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
return (CGSize(width: refinedWidth, height: 33.0), {
targetNode.accessibilityLabel = title
targetNode.titleColor = titleColor
if let updatedRegularImage = updatedRegularImage {
targetNode.regularImage = updatedRegularImage
if !targetNode.textNode.isHidden {
@ -203,8 +232,9 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
let _ = textApply()
let _ = highlightedTextApply()
targetNode.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: 33.0))
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: 33.0))
var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((34.0 - textSize.size.height) / 2.0)), size: textSize.size)
targetNode.backgroundNode.frame = backgroundFrame
if let image = targetNode.iconNode.image {
textFrame.origin.x += floor(image.size.width / 2.0)
targetNode.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + 2.0), size: image.size)
@ -782,7 +812,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColor.fill[0]
}
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, actionTitle, titleColor, titleHighlightedColor)
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, actionTitle, titleColor, titleHighlightedColor, false)
boundingSize.width = max(buttonWidth, boundingSize.width)
continueActionButtonLayout = continueLayout
}
@ -934,8 +964,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
if let updateImageSignal = updateInlineImageSignal {
strongSelf.inlineImageNode.setSignal(updateImageSignal)
}
strongSelf.inlineImageNode.frame = imageFrame
animation.animator.updateFrame(layer: strongSelf.inlineImageNode.layer, frame: imageFrame, completion: nil)
if strongSelf.inlineImageNode.supernode == nil {
strongSelf.addSubnode(strongSelf.inlineImageNode)
}

View File

@ -260,7 +260,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
}
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, titleHighlightedColor)
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, titleHighlightedColor, false)
var maxContentWidth: CGFloat = avatarSize.width + 7.0
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {

View File

@ -644,7 +644,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, !(self.dustNode?.isRevealed ?? true) {
if let spoilerRects = spoilerRects, !spoilerRects.isEmpty, let dustNode = self.dustNode, !dustNode.isRevealed {
} else if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode

View File

@ -56,7 +56,7 @@ final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleContentNod
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColor.fill[0]
}
let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, presentationData.strings.Conversation_UpdateTelegram, titleColor, titleHighlightedColor)
let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, presentationData.strings.Conversation_UpdateTelegram, titleColor, titleHighlightedColor, false)
let initialWidth = buttonWidth + insets.left + insets.right

View File

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

View File

@ -206,7 +206,13 @@ private func CreateChannelEntries(presentationData: PresentationData, state: Cre
return entries
}
public func createChannelController(context: AccountContext) -> ViewController {
public enum CreateChannelMode {
case generic
case requestPeer(ReplyMarkupButtonRequestPeerType.Channel)
}
public func createChannelController(context: AccountContext, mode: CreateChannelMode = .generic, completion: ((PeerId, @escaping () -> Void) -> Void)? = nil) -> ViewController {
let initialState = CreateChannelState(creating: false, editingName: ItemListAvatarAndNameInfoItemName.title(title: "", type: .channel), editingDescriptionText: "", avatar: nil)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@ -455,6 +461,7 @@ public func createChannelController(context: AccountContext) -> ViewController {
}
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)!
mixin.stickersContext = LegacyPaintStickersContext(context: context)
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in

View File

@ -29,6 +29,7 @@ import LegacyMediaPickerUI
import ContextUI
import ChatTimerScreen
import AsyncDisplayKit
import TextFormat
private struct CreateGroupArguments {
let context: AccountContext
@ -39,10 +40,14 @@ private struct CreateGroupArguments {
let changeLocation: () -> Void
let updateWithVenue: (TelegramMediaMap) -> Void
let updateAutoDelete: () -> Void
let updatePublicLinkText: (String) -> Void
let openAuction: (String) -> Void
}
private enum CreateGroupSection: Int32 {
case info
case username
case topics
case autoDelete
case members
case location
@ -65,6 +70,12 @@ private enum CreateGroupEntryTag: ItemListItemTag {
private enum CreateGroupEntry: ItemListNodeEntry {
case groupInfo(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?)
case setProfilePhoto(PresentationTheme, String)
case usernameHeader(PresentationTheme, String)
case username(PresentationTheme, String, String)
case usernameStatus(PresentationTheme, String, AddressNameValidationStatus, String, String)
case usernameInfo(PresentationTheme, String)
case topics(PresentationTheme, String)
case topicsInfo(PresentationTheme, String)
case autoDelete(title: String, value: String)
case autoDeleteInfo(String)
case member(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, PeerPresence?)
@ -79,6 +90,10 @@ private enum CreateGroupEntry: ItemListNodeEntry {
switch self {
case .groupInfo, .setProfilePhoto:
return CreateGroupSection.info.rawValue
case .usernameHeader, .username, .usernameStatus, .usernameInfo:
return CreateGroupSection.username.rawValue
case .topics, .topicsInfo:
return CreateGroupSection.topics.rawValue
case .autoDelete, .autoDeleteInfo:
return CreateGroupSection.autoDelete.rawValue
case .member:
@ -96,12 +111,24 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return 0
case .setProfilePhoto:
return 1
case .autoDelete:
case .usernameHeader:
return 2
case .autoDeleteInfo:
case .username:
return 3
case .usernameStatus:
return 4
case .usernameInfo:
return 5
case .topics:
return 6
case .topicsInfo:
return 7
case .autoDelete:
return 8
case .autoDeleteInfo:
return 9
case let .member(index, _, _, _, _, _, _):
return 4 + index
return 10 + index
case .locationHeader:
return 10000
case .location:
@ -153,6 +180,43 @@ private enum CreateGroupEntry: ItemListNodeEntry {
} else {
return false
}
case let .usernameHeader(lhsTheme, lhsText):
if case let .usernameHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .username(lhsTheme, lhsText, lhsValue):
if case let .username(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .usernameStatus(lhsTheme, lhsAddressName, lhsStatus, lhsText, lhsUsername):
if case let .usernameStatus(rhsTheme, rhsAddressName, rhsStatus, rhsText, rhsUsername) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsStatus == rhsStatus, lhsText == rhsText, lhsUsername == rhsUsername {
return true
} else {
return false
}
case let .usernameInfo(lhsTheme, lhsText):
if case let .usernameInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .topics(lhsTheme, lhsText):
if case let .topics(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .topicsInfo(lhsTheme, lhsText):
if case let .topicsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .autoDelete(title, value):
if case .autoDelete(title, value) = rhs {
return true
@ -263,6 +327,41 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.changeProfilePhoto()
})
case let .usernameHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .username(theme, placeholder, text):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: placeholder, type: .username, clearType: .always, tag: nil, sectionId: self.section, textUpdated: { updatedText in
arguments.updatePublicLinkText(updatedText)
}, action: {
})
case let .usernameStatus(_, _, status, text, username):
var displayActivity = false
let textColor: ItemListActivityTextItem.TextColor
switch status {
case .invalidFormat:
textColor = .destructive
case let .availability(availability):
switch availability {
case .available:
textColor = .constructive
case .purchaseAvailable:
textColor = .generic
case .invalid, .taken:
textColor = .destructive
}
case .checking:
textColor = .generic
displayActivity = true
}
return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: text, color: textColor, linkAction: { _ in
arguments.openAuction(username)
}, sectionId: self.section)
case let .usernameInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .topics(_, text):
return ItemListSwitchItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Topics")?.precomposed(), title: text, value: true, enabled: false, sectionId: self.section, style: .blocks, updated: { _ in })
case let .topicsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .autoDelete(text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
arguments.updateAutoDelete()
@ -299,9 +398,11 @@ private struct CreateGroupState: Equatable {
var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?
var location: PeerGeoLocation?
var autoremoveTimeout: Int32?
var editingPublicLinkText: String?
var addressNameValidationStatus: AddressNameValidationStatus?
}
private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView, venues: [TelegramMediaMap]?, globalAutoremoveTimeout: Int32) -> [CreateGroupEntry] {
private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView, venues: [TelegramMediaMap]?, globalAutoremoveTimeout: Int32, requestPeer: ReplyMarkupButtonRequestPeerType.Group?) -> [CreateGroupEntry] {
var entries: [CreateGroupEntry] = []
let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil)
@ -310,6 +411,58 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat
entries.append(.groupInfo(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, groupInfoState, state.avatar))
if let requestPeer {
if let hasUsername = requestPeer.hasUsername, hasUsername {
let currentUsername = state.editingPublicLinkText ?? ""
entries.append(.usernameHeader(presentationData.theme, presentationData.strings.CreateGroup_PublicLinkTitle.uppercased()))
entries.append(.username(presentationData.theme, "link", currentUsername))
if let status = state.addressNameValidationStatus {
let statusText: String
switch status {
case let .invalidFormat(error):
switch error {
case .startsWithDigit:
statusText = presentationData.strings.Username_InvalidStartsWithNumber
case .startsWithUnderscore:
statusText = presentationData.strings.Username_InvalidStartsWithUnderscore
case .endsWithUnderscore:
statusText = presentationData.strings.Username_InvalidEndsWithUnderscore
case .invalidCharacters:
statusText = presentationData.strings.Username_InvalidCharacters
case .tooShort:
statusText = presentationData.strings.Username_InvalidTooShort
}
case let .availability(availability):
switch availability {
case .available:
statusText = presentationData.strings.Username_UsernameIsAvailable(currentUsername).string
case .invalid:
statusText = presentationData.strings.Username_InvalidCharacters
case .taken:
statusText = presentationData.strings.Username_InvalidTaken
case .purchaseAvailable:
var markdownString = presentationData.strings.Username_UsernamePurchaseAvailable
let entities = generateTextEntities(markdownString, enabledTypes: [.mention])
if let entity = entities.first {
markdownString.insert(contentsOf: "]()", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.upperBound))
markdownString.insert(contentsOf: "[", at: markdownString.index(markdownString.startIndex, offsetBy: entity.range.lowerBound))
}
statusText = markdownString
}
case .checking:
statusText = presentationData.strings.Username_CheckingUsername
}
entries.append(.usernameStatus(presentationData.theme, currentUsername, status, statusText, currentUsername))
}
entries.append(.usernameInfo(presentationData.theme, presentationData.strings.CreateGroup_PublicLinkInfo))
}
if let isForum = requestPeer.isForum, isForum {
entries.append(.topics(presentationData.theme, presentationData.strings.PeerInfo_OptionTopics))
entries.append(.topicsInfo(presentationData.theme, presentationData.strings.PeerInfo_OptionTopicsText))
}
} else {
let autoremoveTimeout = state.autoremoveTimeout ?? globalAutoremoveTimeout
let autoRemoveText: String
if autoremoveTimeout == 0 {
@ -319,6 +472,7 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat
}
entries.append(.autoDelete(title: presentationData.strings.CreateGroup_AutoDeleteTitle, value: autoRemoveText))
entries.append(.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,8 +586,8 @@ 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 {
@ -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,6 +653,81 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
}
}
case let .requestPeer(group):
var isForum = false
if let isForumRequested = group.isForum, isForumRequested {
isForum = true
}
if isForum {
createSignal = context.engine.peers.createSupergroup(title: title, description: nil, isForum: true)
|> map(Optional.init)
|> mapError { error -> CreateGroupError in
switch error {
case .generic:
return .generic
case .restricted:
return .restricted
case .tooMuchJoined:
return .tooMuchJoined
case .tooMuchLocationBasedGroups:
return .tooMuchLocationBasedGroups
case let .serverProvided(error):
return .serverProvided(error)
}
}
if let publicLink, !publicLink.isEmpty {
createSignal = createSignal
|> mapToSignal { peerId in
if let peerId = peerId {
return context.engine.peers.updateAddressName(domain: .peer(peerId), name: publicLink)
|> mapError { _ in
return .generic
}
|> map { _ in
return peerId
}
} else {
return .fail(.generic)
}
}
}
} else {
if let publicLink, !publicLink.isEmpty {
createSignal = context.engine.peers.createSupergroup(title: title, description: nil)
|> map(Optional.init)
|> mapError { error -> CreateGroupError in
switch error {
case .generic:
return .generic
case .restricted:
return .restricted
case .tooMuchJoined:
return .tooMuchJoined
case .tooMuchLocationBasedGroups:
return .tooMuchLocationBasedGroups
case let .serverProvided(error):
return .serverProvided(error)
}
}
|> mapToSignal { peerId in
if let peerId = peerId {
return context.engine.peers.updateAddressName(domain: .peer(peerId), name: publicLink)
|> mapError { _ in
return .generic
}
|> map { _ in
return peerId
}
} else {
return .fail(.generic)
}
}
} else {
createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds, ttlPeriod: nil)
}
}
}
actionsDisposable.add((createSignal
@ -728,6 +960,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)!
mixin.stickersContext = LegacyPaintStickersContext(context: context)
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in
@ -910,8 +1143,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 +1188,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 +1198,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 +1207,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
let controller = ItemListController(context: context, state: signal)
controller.beganInteractiveDragging = {
endEditingImpl?()
}
replaceControllerImpl = { [weak controller] value in
(controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true)
}

View File

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

View File

@ -2728,6 +2728,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _ in
}, openRequestedPeerSelection: { _, _, _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {
@ -3094,7 +3095,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
}
if peer.firstName != firstName || peer.lastName != lastName || (bio != nil && bio != cachedData.about) {
if (peer.firstName ?? "") != firstName || (peer.lastName ?? "") != lastName || (bio ?? "") != (cachedData.about ?? "") {
var updateNameSignal: Signal<Void, NoError> = .complete()
var hasProgress = false
if peer.firstName != firstName || peer.lastName != lastName {
@ -7159,8 +7160,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
hasPhotos = true
}
let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context)
var isForum = false
if let peer = strongSelf.data?.peer as? TelegramChannel, peer.flags.contains(.isForum) {
isForum = true
@ -7200,7 +7199,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasDeleteButton, hasViewButton: false, personalPhoto: strongSelf.isSettings, isVideo: currentIsVideo, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: isForum, title: title, isSuggesting: [.custom, .suggest].contains(mode))!
mixin.stickersContext = paintStickersContext
mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context)
let _ = strongSelf.currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { [weak self] assetsController in
guard let strongSelf = self else {

View File

@ -62,6 +62,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
private let pretendPresentedInModal: Bool
private let forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
override public var _presentedInModal: Bool {
get {
@ -93,12 +94,29 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
self.forwardedMessageIds = params.forwardedMessageIds
self.hasTypeHeaders = params.hasTypeHeaders
self.selectForumThreads = params.selectForumThreads
self.requestPeerType = params.requestPeerType
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.customTitle = params.title
if let peerType = params.requestPeerType {
switch peerType {
case let .user(user):
if let isBot = user.isBot, isBot {
self.customTitle = self.presentationData.strings.RequestPeer_ChooseBotTitle
} else {
self.customTitle = self.presentationData.strings.RequestPeer_ChooseUserTitle
}
case .group:
self.customTitle = self.presentationData.strings.RequestPeer_ChooseGroupTitle
case .channel:
self.customTitle = self.presentationData.strings.RequestPeer_ChooseChannelTitle
}
}
self.title = self.customTitle ?? self.presentationData.strings.Conversation_ForwardTitle
if params.forumPeerId == nil {
@ -159,7 +177,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon
}
override public func loadDisplayNode() {
self.displayNode = PeerSelectionControllerNode(context: self.context, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, createNewGroup: self.createNewGroup, present: { [weak self] c, a in
self.displayNode = PeerSelectionControllerNode(context: self.context, controller: self, presentationData: self.presentationData, filter: self.filter, forumPeerId: self.forumPeerId, hasChatListSelector: self.hasChatListSelector, hasContactSelector: self.hasContactSelector, hasGlobalSearch: self.hasGlobalSearch, forwardedMessageIds: self.forwardedMessageIds, hasTypeHeaders: self.hasTypeHeaders, requestPeerType: self.requestPeerType, createNewGroup: self.createNewGroup, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}, presentInGlobalOverlay: { [weak self] c, a in
self?.presentInGlobalOverlay(c, with: a)

View File

@ -18,9 +18,13 @@ import ChatSendMessageActionUI
import ChatTextLinkEditUI
import AnimationCache
import MultiAnimationRenderer
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
final class PeerSelectionControllerNode: ASDisplayNode {
private let context: AccountContext
private weak var controller: PeerSelectionController?
private let present: (ViewController, Any?) -> Void
private let presentInGlobalOverlay: (ViewController, Any?) -> Void
private let dismiss: () -> Void
@ -29,6 +33,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
private let hasGlobalSearch: Bool
private let forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private var presentationInterfaceState: ChatPresentationInterfaceState
private var interfaceInteraction: ChatPanelInterfaceInteraction?
@ -41,6 +46,16 @@ final class PeerSelectionControllerNode: ASDisplayNode {
var navigationBar: NavigationBar?
private let requirementsBackgroundNode: NavigationBackgroundNode?
private let requirementsSeparatorNode: ASDisplayNode?
private let requirementsTextNode: ImmediateTextNode?
private let emptyAnimationNode: AnimatedStickerNode
private var emptyAnimationSize = CGSize()
private let emptyTitleNode: ImmediateTextNode
private let emptyTextNode: ImmediateTextNode
private let emptyButtonNode: SolidRoundedButtonNode
private let toolbarBackgroundNode: NavigationBackgroundNode?
private let toolbarSeparatorNode: ASDisplayNode?
private let segmentedControlNode: SegmentedControlNode?
@ -83,12 +98,15 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return self.readyValue.get()
}
private var isEmpty = false
private var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) {
return (self.presentationData, self.presentationDataPromise.get())
}
init(context: AccountContext, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
init(context: AccountContext, controller: PeerSelectionController, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: ReplyMarkupButtonRequestPeerType?, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
self.context = context
self.controller = controller
self.present = present
self.presentInGlobalOverlay = presentInGlobalOverlay
self.dismiss = dismiss
@ -97,6 +115,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.hasGlobalSearch = hasGlobalSearch
self.forwardedMessageIds = forwardedMessageIds
self.hasTypeHeaders = hasTypeHeaders
self.requestPeerType = requestPeerType
self.presentationData = presentationData
@ -107,6 +126,43 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) }
if let _ = self.requestPeerType {
self.requirementsBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
self.requirementsSeparatorNode = ASDisplayNode()
self.requirementsSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.requirementsTextNode = ImmediateTextNode()
self.requirementsTextNode?.maximumNumberOfLines = 0
self.requirementsTextNode?.lineSpacing = 0.1
} else {
self.requirementsBackgroundNode = nil
self.requirementsSeparatorNode = nil
self.requirementsTextNode = nil
}
self.emptyTitleNode = ImmediateTextNode()
self.emptyTitleNode.displaysAsynchronously = false
self.emptyTitleNode.maximumNumberOfLines = 0
self.emptyTitleNode.isHidden = true
self.emptyTitleNode.textAlignment = .center
self.emptyTitleNode.lineSpacing = 0.25
self.emptyTextNode = ImmediateTextNode()
self.emptyTextNode.displaysAsynchronously = false
self.emptyTextNode.maximumNumberOfLines = 0
self.emptyTextNode.isHidden = true
self.emptyTextNode.lineSpacing = 0.25
self.emptyAnimationNode = DefaultAnimatedStickerNodeImpl()
self.emptyAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListNoResults"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.emptyAnimationNode.isHidden = true
self.emptyAnimationSize = CGSize(width: 120.0, height: 120.0)
self.emptyButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), cornerRadius: 11.0, gloss: true)
self.emptyButtonNode.isHidden = true
self.emptyButtonNode.pressed = {
createNewGroup?()
}
if hasChatListSelector && hasContactSelector {
self.toolbarBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
@ -137,7 +193,14 @@ final class PeerSelectionControllerNode: ASDisplayNode {
chatListLocation = .chatList(groupId: .root)
}
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
let chatListMode: ChatListNodeMode
if let requestPeerType = self.requestPeerType {
chatListMode = .peerType(type: requestPeerType)
} else {
chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false)
}
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
super.init()
@ -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,182 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}
}
}
func stringForAdminRights(strings: PresentationStrings, adminRights: TelegramChatAdminRights) -> String {
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 {
return String(rights.joined(separator: ", "))
} else {
return ""
}
}
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, !group.isCreator {
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 let adminRights = channel.userAdminRights, !channel.isCreator {
var rights: [String] = []
if adminRights.rights.contains(.canChangeInfo) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Info)
}
if adminRights.rights.contains(.canPostMessages) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Send)
}
if adminRights.rights.contains(.canDeleteMessages) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Delete)
}
if adminRights.rights.contains(.canEditMessages) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Edit)
}
if adminRights.rights.contains(.canBanUsers) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Ban)
}
if adminRights.rights.contains(.canInviteUsers) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Invite)
}
if adminRights.rights.contains(.canPinMessages) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Pin)
}
if adminRights.rights.contains(.canManageTopics) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Topics)
}
if adminRights.rights.contains(.canManageCalls) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_VideoChats)
}
if adminRights.rights.contains(.canBeAnonymous) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Anonymous)
}
if adminRights.rights.contains(.canAddAdmins) {
rights.append(strings.RequestPeer_Requirement_Channel_Rights_AddAdmins)
}
if !rights.isEmpty {
let rightsString = strings.RequestPeer_Requirement_Group_Rights(String(rights.joined(separator: ", "))).string
append(rightsString)
}
}
}
if lines.isEmpty {
return nil
} else {
return String(lines.joined(separator: "\n"))
}
}

View File

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

View File

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

View File

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