diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 645301d9c9..4823335c21 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -155,6 +155,7 @@ public struct ChatAvailableMessageActionOptions: OptionSet { public static let sendScheduledNow = ChatAvailableMessageActionOptions(rawValue: 1 << 8) public static let editScheduledTime = ChatAvailableMessageActionOptions(rawValue: 1 << 9) public static let externalShare = ChatAvailableMessageActionOptions(rawValue: 1 << 10) + public static let sendGift = ChatAvailableMessageActionOptions(rawValue: 1 << 11) } public struct ChatAvailableMessageActions { @@ -802,6 +803,7 @@ public protocol TelegramRootControllerInterface: NavigationController { func getPrivacySettings() -> Promise? func openSettings() func openBirthdaySetup() + func openPhotoSetup() } public protocol QuickReplySetupScreenInitialData: AnyObject { diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 5369f4d437..b03a2b51e5 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -959,6 +959,7 @@ public protocol PeerInfoScreen: ViewController { func toggleStorySelection(ids: [Int32], isSelected: Bool) func togglePaneIsReordering(isReordering: Bool) func cancelItemSelection() + func openAvatarSetup() } public extension Peer { diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 2da10e6f95..24523c4537 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -24,6 +24,7 @@ private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "A private let anonymousSavedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: .white) private let anonymousSavedMessagesDarkIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: UIColor(white: 1.0, alpha: 0.4)) private let myNotesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/MyNotesIcon"), color: .white) +private let cameraIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/CameraIcon"), color: .white) public func avatarPlaceholderFont(size: CGFloat) -> UIFont { return Font.with(size: size, design: .round, weight: .bold) @@ -86,7 +87,7 @@ public func calculateAvatarColors(context: AccountContext?, explicitColorIndex: } let colors: [UIColor] - if icon != .none { + if icon != .none && icon != .cameraIcon { if case .deletedIcon = icon { colors = AvatarNode.grayscaleColors } else if case .phoneIcon = icon { @@ -196,6 +197,7 @@ public enum AvatarNodeIcon: Equatable { case deletedIcon case phoneIcon case repostIcon + case cameraIcon } public enum AvatarNodeImageOverride: Equatable { @@ -210,6 +212,7 @@ public enum AvatarNodeImageOverride: Equatable { case deletedIcon case phoneIcon case repostIcon + case cameraIcon } public enum AvatarNodeColorOverride { @@ -540,6 +543,9 @@ public final class AvatarNode: ASDisplayNode { case .phoneIcon: representation = nil icon = .phoneIcon + case .cameraIcon: + representation = nil + icon = .cameraIcon } } else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil { representation = peer?.smallProfileImage @@ -716,6 +722,9 @@ public final class AvatarNode: ASDisplayNode { case .phoneIcon: representation = nil icon = .phoneIcon + case .cameraIcon: + representation = nil + icon = .cameraIcon } } else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil { representation = peer?.smallProfileImage @@ -959,6 +968,15 @@ public final class AvatarNode: ASDisplayNode { if let myNotesIcon = myNotesIcon { context.draw(myNotesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - myNotesIcon.size.width) / 2.0), y: floor((bounds.size.height - myNotesIcon.size.height) / 2.0)), size: myNotesIcon.size)) } + } else if case .cameraIcon = parameters.icon { + let factor = bounds.size.width / 40.0 + context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) + context.scaleBy(x: factor, y: -factor) + context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) + + if let cameraIcon = cameraIcon { + context.draw(cameraIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - cameraIcon.size.width) / 2.0), y: floor((bounds.size.height - cameraIcon.size.height) / 2.0)), size: cameraIcon.size)) + } } else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage { context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index 08021c7680..8bc74ceefc 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -246,7 +246,9 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P } } - context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + let filledSize = CGSize(width: dataImage.width, height: dataImage.height).aspectFilled(displayDimensions) + + context.draw(dataImage, in: CGRect(origin: CGPoint(x: floor((displayDimensions.width - filledSize.width) / 2.0), y: floor((displayDimensions.height - filledSize.height) / 2.0)), size: filledSize).insetBy(dx: inset, dy: inset)) if blurred { context.setBlendMode(.normal) context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.45).cgColor) diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index a18715d449..c763da34db 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -162,6 +162,7 @@ public final class BrowserBookmarksScreen: ViewController { }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in + }, sendGift: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift index a6a5ffcfad..d977ba902a 100644 --- a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift +++ b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift @@ -214,7 +214,8 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { theme: PresentationTheme, mode: HorizontalPeerItemMode, strings: PresentationStrings, - peerSelected: @escaping (EnginePeer) -> Void, peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, isPeerSelected: @escaping (EnginePeer.Id) -> Bool, share: Bool = false) { + peerSelected: @escaping (EnginePeer) -> Void, peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, isPeerSelected: @escaping (EnginePeer.Id) -> Bool, share: Bool = false) + { self.theme = theme self.strings = strings self.themeAndStringsPromise = Promise((self.theme, self.strings)) @@ -225,6 +226,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { self.isPeerSelected = isPeerSelected self.listView = ListView() + self.listView.preloadPages = false self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.listView.accessibilityPageScrolledString = { row, count in return strings.VoiceOver_ScrollStatus(row, count).string @@ -340,11 +342,6 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { ) strongSelf.enqueueTransition(transition) - - if !strongSelf.didSetReady { - strongSelf.ready.set(.single(true)) - strongSelf.didSetReady = true - } } })) if case .actionSheet = mode { @@ -371,7 +368,20 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { } else if transition.animated { options.insert(.AnimateInsertion) } - self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { _ in }) + self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + guard let self else { + return + } + if !self.didSetReady { + self.ready.set(.single(true)) + self.didSetReady = true + } + if !self.listView.preloadPages { + Queue.mainQueue().after(0.5) { + self.listView.preloadPages = true + } + } + }) } } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 0565b89ee7..d46cf25f11 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1202,6 +1202,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ) } + self.chatListDisplayNode.mainContainerNode.openPhotoSetup = { [weak self] in + guard let self else { + return + } + if let rootController = self.navigationController as? TelegramRootControllerInterface { + rootController.openPhotoSetup() + } + } + self.chatListDisplayNode.mainContainerNode.openPremiumManagement = { [weak self] in guard let self else { return diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index f6a06ce313..aff406f13d 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -354,6 +354,9 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele itemNode.listNode.openWebApp = { [weak self] amount in self?.openWebApp?(amount) } + itemNode.listNode.openPhotoSetup = { [weak self] in + self?.openPhotoSetup?() + } self.currentItemStateValue.set(itemNode.listNode.state |> map { state in let filterId: Int32? @@ -421,6 +424,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)? var openStarsTopup: ((Int64?) -> Void)? var openWebApp: ((TelegramUser) -> Void)? + var openPhotoSetup: (() -> Void)? var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? var didBeginSelectingChats: (() -> Void)? var canExpandHiddenItems: (() -> Bool)? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 3120147528..1f81618ce4 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -115,7 +115,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, - peerSelected: @escaping (EnginePeer, Int64?, Bool) -> Void, + peerSelected: @escaping (EnginePeer, Int64?, Bool, OpenPeerAction) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, @@ -130,7 +130,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { switch self { case let .topPeers(peers, theme, strings): return ChatListRecentPeersListItem(theme: theme, strings: strings, context: context, peers: peers, peerSelected: { peer in - peerSelected(peer, nil, false) + peerSelected(peer, nil, false, .generic) }, peerContextAction: { peer, node, gesture, location in if let peerContextAction = peerContextAction { peerContextAction(peer, .recentPeers, node, gesture, location) @@ -287,21 +287,28 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } var buttonAction: ContactsPeerItemButtonAction? - if case .chats = key, case let .user(user) = primaryPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { + if [.chats, .apps].contains(key), case let .user(user) = primaryPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) { buttonAction = ContactsPeerItemButtonAction( title: presentationData.strings.ChatList_Search_Open, action: { peer, _, _ in - peerSelected(primaryPeer, nil, true) + peerSelected(primaryPeer, nil, false, .openApp) } ) } + var peerMode: ContactsPeerItemPeerMode + if case .apps = key { + peerMode = .app(isPopular: section == .popularApps) + } else { + peerMode = .generalSearch(isSavedMessages: false) + } + return ContactsPeerItem( presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, - peerMode: .generalSearch(isSavedMessages: false), + peerMode: peerMode, peer: .peer(peer: primaryPeer, chatPeer: chatPeer), status: status, badge: badge, @@ -315,7 +322,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { alwaysShowLastSeparator: key == .apps, action: { _ in if let chatPeer = peer.peer.peers[peer.peer.peerId] { - peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels || section == .popularApps) + peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels, section == .popularApps ? .info : .generic) } }, disabledAction: { _ in @@ -1067,6 +1074,12 @@ public struct ChatListSearchContainerTransition { } } +enum OpenPeerAction { + case generic + case info + case openApp +} + private func chatListSearchContainerPreparedRecentTransition( from fromEntries: [ChatListRecentEntry], to toEntries: [ChatListRecentEntry], @@ -1075,7 +1088,7 @@ private func chatListSearchContainerPreparedRecentTransition( presentationData: ChatListPresentationData, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, - peerSelected: @escaping (EnginePeer, Int64?, Bool) -> Void, + peerSelected: @escaping (EnginePeer, Int64?, Bool, OpenPeerAction) -> Void, disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?, clearRecentlySearchedPeers: @escaping () -> Void, @@ -1468,6 +1481,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.selectedMessagesPromise.set(.single(self.selectedMessages)) self.recentListNode = ListView() + self.recentListNode.preloadPages = false self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor self.recentListNode.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).string @@ -3014,6 +3028,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, dismissNotice: { _ in }, editPeer: { _ in }, openWebApp: { _ in + }, openPhotoSetup: { }) chatListInteraction.isSearchMode = true @@ -3822,7 +3837,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } - let transition = chatListSearchContainerPreparedRecentTransition(from: previousRecentItems?.entries ?? [], to: recentItems.entries, forceUpdateAll: forceUpdateAll, context: context, presentationData: presentationData, filter: peersFilter, key: key, peerSelected: { peer, threadId, isRecommended in + let transition = chatListSearchContainerPreparedRecentTransition(from: previousRecentItems?.entries ?? [], to: recentItems.entries, forceUpdateAll: forceUpdateAll, context: context, presentationData: presentationData, filter: peersFilter, key: key, peerSelected: { peer, threadId, isRecommended, action in guard let self else { return } @@ -3848,36 +3863,39 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } else if case .apps = key { if let navigationController = self.navigationController { - if isRecommended { - if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { - navigationController.pushViewController(peerInfoScreen) - } - } else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController { - self.context.sharedContext.openWebApp( - context: self.context, - parentController: parentController, - updatedPresentationData: nil, - botPeer: peer, - chatPeer: nil, - threadId: nil, - buttonText: "", - url: "", - simple: true, - source: .generic, - skipTermsOfService: true, - payload: nil - ) - } else { + switch action { + case .generic: self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .always )) + case .info: + if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + navigationController.pushViewController(peerInfoScreen) + } + case .openApp: + if let parentController = self.parentController { + self.context.sharedContext.openWebApp( + context: self.context, + parentController: parentController, + updatedPresentationData: nil, + botPeer: peer, + chatPeer: nil, + threadId: nil, + buttonText: "", + url: "", + simple: true, + source: .generic, + skipTermsOfService: true, + payload: nil + ) + } } } } else { - if isRecommended, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController { + if case .openApp = action, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController { self.context.sharedContext.openWebApp( context: self.context, parentController: parentController, @@ -4581,6 +4599,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { strongSelf.ready.set(.single(true)) } strongSelf.didSetReady = true + if !strongSelf.recentListNode.preloadPages { + Queue.mainQueue().after(0.5) { + strongSelf.recentListNode.preloadPages = true + } + } strongSelf.emptyRecentAnimationNode?.isHidden = !transition.isEmpty strongSelf.emptyRecentTitleNode?.isHidden = !transition.isEmpty @@ -4907,6 +4930,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { }, dismissNotice: { _ in }, editPeer: { _ in }, openWebApp: { _ in + }, openPhotoSetup: { }) var isInlineMode = false if case .topics = key { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 11d4fafc1d..43a31277e4 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -160,6 +160,7 @@ public final class ChatListShimmerNode: ASDisplayNode { }, dismissNotice: { _ in }, editPeer: { _ in }, openWebApp: { _ in + }, openPhotoSetup: { }) interaction.isInlineMode = isInlineMode diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 0c0e1111b8..9d33d16d27 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -113,6 +113,7 @@ public final class ChatListNodeInteraction { let dismissNotice: (ChatListNotice) -> Void let editPeer: (ChatListItem) -> Void let openWebApp: (TelegramUser) -> Void + let openPhotoSetup: () -> Void public var searchTextHighightState: String? var highlightedChatLocation: ChatListHighlightedLocation? @@ -169,7 +170,8 @@ public final class ChatListNodeInteraction { openStarsTopup: @escaping (Int64?) -> Void, dismissNotice: @escaping (ChatListNotice) -> Void, editPeer: @escaping (ChatListItem) -> Void, - openWebApp: @escaping (TelegramUser) -> Void + openWebApp: @escaping (TelegramUser) -> Void, + openPhotoSetup: @escaping () -> Void ) { self.activateSearch = activateSearch self.peerSelected = peerSelected @@ -214,6 +216,7 @@ public final class ChatListNodeInteraction { self.dismissNotice = dismissNotice self.editPeer = editPeer self.openWebApp = openWebApp + self.openPhotoSetup = openPhotoSetup } } @@ -519,10 +522,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL default: break } - } - - if canManage { } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { + canManage = true + } else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) { + canManage = true + } + + if canManage { } else { enabled = false } @@ -759,6 +765,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL break case let .starsSubscriptionLowBalance(amount, _): nodeInteraction?.openStarsTopup(amount.value) + case .setupPhoto: + nodeInteraction?.openPhotoSetup() } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1103,6 +1111,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL break case let .starsSubscriptionLowBalance(amount, _): nodeInteraction?.openStarsTopup(amount.value) + case .setupPhoto: + nodeInteraction?.openPhotoSetup() } case .hide: nodeInteraction?.dismissNotice(notice) @@ -1224,6 +1234,7 @@ public final class ChatListNode: ListView { public var openPremiumManagement: (() -> Void)? public var openStarsTopup: ((Int64?) -> Void)? public var openWebApp: ((TelegramUser) -> Void)? + public var openPhotoSetup: (() -> Void)? private var theme: PresentationTheme @@ -1876,6 +1887,11 @@ public final class ChatListNode: ListView { return } self.openWebApp?(user) + }, openPhotoSetup: { [weak self] in + guard let self else { + return + } + self.openPhotoSetup?() }) nodeInteraction.isInlineMode = isInlineMode @@ -1970,11 +1986,16 @@ public final class ChatListNode: ListView { context.engine.notices.getServerDismissedSuggestions(), twoStepData, newSessionReviews(postbox: context.account.postbox), - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)), + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId) + ), context.account.stateManager.contactBirthdays, starsSubscriptionsContextPromise.get() ) - |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, birthday, birthdays, starsSubscriptionsContext -> Signal in + |> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext -> Signal in + let (accountPeer, birthday) = data + if let newSessionReview = newSessionReviews.first { return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count)) } @@ -2025,6 +2046,8 @@ public final class ChatListNode: ListView { starsSubscriptionsContextPromise.set(.single(context.engine.payments.peerStarsSubscriptionsContext(starsContext: nil, missingBalance: true))) return .single(nil) } + } else if suggestions.contains(.setupPhoto), let accountPeer, accountPeer.smallProfileImage == nil { + return .single(.setupPhoto(accountPeer)) } else if suggestions.contains(.gracePremium) { return .single(.premiumGrace) } else if suggestions.contains(.setupBirthday) && birthday == nil { @@ -2413,10 +2436,13 @@ public final class ChatListNode: ListView { default: break } + } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { + canManage = true + } else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) { + canManage = true } if canManage { - } else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) { } else { return false } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index bc44e64ec5..4a338533a2 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -91,6 +91,7 @@ public enum ChatListNotice: Equatable { case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) case premiumGrace case starsSubscriptionLowBalance(amount: StarsAmount, peers: [EnginePeer]) + case setupPhoto(EnginePeer) } enum ChatListNodeEntry: Comparable, Identifiable { @@ -605,6 +606,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, var result: [ChatListNodeEntry] = [] + var hasContacts = false if !view.hasEarlier { var existingPeerIds = Set() for item in view.items { @@ -620,8 +622,9 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState, peer: contact.peer, presence: contact.presence ))) + hasContacts = true } - if !contacts.isEmpty { + if hasContacts { result.append(.SectionHeader(presentationData: state.presentationData, displayHide: !view.items.isEmpty)) } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index 7ba967c6ea..65a0f6f330 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -13,6 +13,7 @@ import AccountContext import MergedAvatarsNode import TextNodeWithEntities import TextFormat +import AvatarNode class ChatListNoticeItem: ListViewItem { enum Action { @@ -94,6 +95,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { private let arrowNode: ASImageNode private let separatorNode: ASDisplayNode + private var avatarNode: AvatarNode? private var avatarsNode: MergedAvatarsNode? private var closeButton: HighlightableButtonNode? @@ -175,6 +177,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { let titleString: NSAttributedString let textString: NSAttributedString + var avatarPeer: EnginePeer? var avatarPeers: [EnginePeer] = [] var okButtonLayout: (TextNodeLayout, () -> TextNode)? @@ -285,12 +288,20 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { } titleString = attributedTitle textString = NSAttributedString(string: text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + case let .setupPhoto(accountPeer): + //TODO:localize + titleString = NSAttributedString(string: "Add your photo! 📸", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) + textString = NSAttributedString(string: "Help your friends spot you easily.", font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + avatarPeer = accountPeer } var leftInset: CGFloat = sideInset if !avatarPeers.isEmpty { let avatarsWidth = 30.0 + CGFloat(avatarPeers.count - 1) * 16.0 leftInset += avatarsWidth + 4.0 + } else if let _ = avatarPeer { + let avatarsWidth: CGFloat = 40.0 + leftInset += avatarsWidth + 6.0 } let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - titleRightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18)) @@ -349,6 +360,25 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { strongSelf.avatarsNode = nil } + if let avatarPeer { + let avatarNode: AvatarNode + if let current = strongSelf.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) + avatarNode.isUserInteractionEnabled = false + strongSelf.addSubnode(avatarNode) + strongSelf.avatarNode = avatarNode + + avatarNode.setPeer(context: item.context, theme: item.theme, peer: avatarPeer, overrideImage: .cameraIcon) + } + let avatarSize = CGSize(width: 40.0, height: 40.0) + avatarNode.frame = CGRect(origin: CGPoint(x: sideInset - 6.0, y: floor((layout.size.height - avatarSize.height) / 2.0)), size: avatarSize) + } else if let avatarNode = strongSelf.avatarNode { + avatarNode.removeFromSupernode() + strongSelf.avatarNode = nil + } + if let image = strongSelf.arrowNode.image { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size) } diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 6536a80b34..33a1422a7f 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -1277,9 +1277,20 @@ public final class ChatPresentationInterfaceState: Equatable { } } +public func canBypassRestrictions(chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool { + guard let boostsToUnrestrict = chatPresentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0 else { + return false + } + if let appliedBoosts = chatPresentationInterfaceState.appliedBoosts, appliedBoosts >= boostsToUnrestrict { + return true + } + return false +} + public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bool { if let peer = state.renderedPeer?.peer { - if canSendMessagesToPeer(peer) { + let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: state) + if canSendMessagesToPeer(peer, ignoreDefault: canBypassRestrictions) { return true } else { return false diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index d15bf6e006..856da22908 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -68,6 +68,7 @@ public enum ContactsPeerItemPeerMode: Equatable { case generalSearch(isSavedMessages: Bool) case peer case memberList + case app(isPopular: Bool) } public enum ContactsPeerItemBadgeType { @@ -722,7 +723,15 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) let titleBoldFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) - let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0)) + + let statusFontSize: CGFloat + if case .app = item.peerMode { + statusFontSize = 15.0 + } else { + statusFontSize = 13.0 + } + let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * statusFontSize / 17.0)) + let badgeFont = Font.regular(14.0) let avatarDiameter = min(40.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 40.0 / 17.0)) @@ -1039,8 +1048,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - let titleVerticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0 - let verticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0 + var verticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0 + if case .app = item.peerMode { + verticalInset += 2.0 + } let statusHeightComponent: CGFloat if statusAttributedString == nil { @@ -1058,7 +1069,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let titleFrame: CGRect if statusAttributedString != nil { - titleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleVerticalInset), size: titleLayout.size) + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size) } else { titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) } @@ -1136,14 +1147,19 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } else if peer.isDeleted { overrideImage = .deletedIcon } + + var displayDimensions = CGSize(width: 60.0, height: 60.0) let clipStyle: AvatarNodeClipStyle - if case let .channel(channel) = peer, channel.flags.contains(.isForum) { + if case .app(true) = item.peerMode { + clipStyle = .roundedRect + displayDimensions = CGSize(width: displayDimensions.width, height: displayDimensions.width * 1.2) + } else if case let .channel(channel) = peer, channel.flags.contains(.isForum) { clipStyle = .roundedRect } else { clipStyle = .round } - strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoads) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoads, displayDimensions: displayDimensions) } case let .deviceContact(_, contact): let letters: [String] @@ -1230,7 +1246,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + var avatarSize: CGSize + if case .app(true) = item.peerMode { + avatarSize = CGSize(width: avatarDiameter, height: avatarDiameter * 1.2) + } else { + avatarSize = CGSize(width: avatarDiameter, height: avatarDiameter) + } + + let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarSize.height) / 2.0)), size: avatarSize) strongSelf.avatarNode.frame = CGRect(origin: CGPoint(), size: avatarFrame.size) diff --git a/submodules/Display/Source/NavigationTransitionCoordinator.swift b/submodules/Display/Source/NavigationTransitionCoordinator.swift index b3c2d7873d..89152cc22b 100644 --- a/submodules/Display/Source/NavigationTransitionCoordinator.swift +++ b/submodules/Display/Source/NavigationTransitionCoordinator.swift @@ -97,7 +97,11 @@ final class NavigationTransitionCoordinator { case .Push: self.container.addSubnode(topNode) case .Pop: - self.container.insertSubnode(bottomNode, belowSubnode: topNode) + if topNode.supernode == self.container { + self.container.insertSubnode(bottomNode, belowSubnode: topNode) + } else { + self.container.addSubnode(topNode) + } } if !self.isFlat { diff --git a/submodules/Postbox/Sources/Message.swift b/submodules/Postbox/Sources/Message.swift index 45b9aba366..ec71c30358 100644 --- a/submodules/Postbox/Sources/Message.swift +++ b/submodules/Postbox/Sources/Message.swift @@ -477,6 +477,10 @@ public struct MessageFlags: OptionSet { rawValue |= MessageFlags.IsForumTopic.rawValue } + if flags.contains(StoreMessageFlags.ReactionsArePossible) { + rawValue |= MessageFlags.ReactionsArePossible.rawValue + } + self.rawValue = rawValue } @@ -490,6 +494,7 @@ public struct MessageFlags: OptionSet { public static let CountedAsIncoming = MessageFlags(rawValue: 256) public static let CopyProtected = MessageFlags(rawValue: 512) public static let IsForumTopic = MessageFlags(rawValue: 1024) + public static let ReactionsArePossible = MessageFlags(rawValue: 2048) public static let IsIncomingMask = MessageFlags([.Incoming, .CountedAsIncoming]) } @@ -843,6 +848,10 @@ public struct StoreMessageFlags: OptionSet { rawValue |= StoreMessageFlags.IsForumTopic.rawValue } + if flags.contains(.ReactionsArePossible) { + rawValue |= StoreMessageFlags.ReactionsArePossible.rawValue + } + self.rawValue = rawValue } @@ -856,6 +865,7 @@ public struct StoreMessageFlags: OptionSet { public static let CountedAsIncoming = StoreMessageFlags(rawValue: 256) public static let CopyProtected = StoreMessageFlags(rawValue: 512) public static let IsForumTopic = StoreMessageFlags(rawValue: 1024) + public static let ReactionsArePossible = StoreMessageFlags(rawValue: 2048) public static let IsIncomingMask = StoreMessageFlags([.Incoming, .CountedAsIncoming]) } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index be013664c2..1947bb9034 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -231,6 +231,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView }, dismissNotice: { _ in }, editPeer: { _ in }, openWebApp: { _ in + }, openPhotoSetup: { }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 2c7d812963..65271c04cb 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -380,6 +380,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { }, dismissNotice: { _ in }, editPeer: { _ in }, openWebApp: { _ in + }, openPhotoSetup: { }) func makeChatListItem( diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index b9ef1f3895..9d5b4e75cc 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -537,7 +537,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } dict[-1808510398] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } - dict[721967202] = { return Api.Message.parse_messageService($0) } + dict[-741178048] = { return Api.Message.parse_messageService($0) } dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) } dict[-988359047] = { return Api.MessageAction.parse_messageActionBotAllowed($0) } dict[-1781355374] = { return Api.MessageAction.parse_messageActionChannelCreate($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index a900e2f09d..fb45fde458 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -62,7 +62,7 @@ public extension Api { indirect enum Message: TypeConstructorDescription { case message(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?, effect: Int64?, factcheck: Api.FactCheck?) case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?) - case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, ttlPeriod: Int32?) + case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, reactions: Api.MessageReactions?, ttlPeriod: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -115,9 +115,9 @@ public extension Api { serializeInt32(id, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {peerId!.serialize(buffer, true)} break - case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod): + case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let reactions, let ttlPeriod): if boxed { - buffer.appendInt32(721967202) + buffer.appendInt32(-741178048) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false) @@ -126,6 +126,7 @@ public extension Api { if Int(flags) & Int(1 << 3) != 0 {replyTo!.serialize(buffer, true)} serializeInt32(date, buffer: buffer, boxed: false) action.serialize(buffer, true) + if Int(flags) & Int(1 << 20) != 0 {reactions!.serialize(buffer, true)} if Int(flags) & Int(1 << 25) != 0 {serializeInt32(ttlPeriod!, buffer: buffer, boxed: false)} break } @@ -137,8 +138,8 @@ public extension Api { return ("message", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("viaBusinessBotId", viaBusinessBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any), ("effect", effect as Any), ("factcheck", factcheck as Any)]) case .messageEmpty(let flags, let id, let peerId): return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)]) - case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod): - return ("messageService", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("action", action as Any), ("ttlPeriod", ttlPeriod as Any)]) + case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let reactions, let ttlPeriod): + return ("messageService", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("action", action as Any), ("reactions", reactions as Any), ("ttlPeriod", ttlPeriod as Any)]) } } @@ -300,8 +301,12 @@ public extension Api { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.MessageAction } - var _8: Int32? - if Int(_1!) & Int(1 << 25) != 0 {_8 = reader.readInt32() } + var _8: Api.MessageReactions? + if Int(_1!) & Int(1 << 20) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.MessageReactions + } } + var _9: Int32? + if Int(_1!) & Int(1 << 25) != 0 {_9 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil @@ -309,9 +314,10 @@ public extension Api { let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 25) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, replyTo: _5, date: _6!, action: _7!, ttlPeriod: _8) + let _c8 = (Int(_1!) & Int(1 << 20) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 25) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, replyTo: _5, date: _6!, action: _7!, reactions: _8, ttlPeriod: _9) } else { return nil diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index d37ba11013..a348b6c1e8 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -135,7 +135,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? { } else { return nil } - case let .messageService(_, _, _, chatPeerId, _, _, _, _): + case let .messageService(_, _, _, chatPeerId, _, _, _, _, _): return chatPeerId.peerId } } @@ -216,7 +216,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { return result case .messageEmpty: return [] - case let .messageService(_, _, fromId, chatPeerId, _, _, action, _): + case let .messageService(_, _, fromId, chatPeerId, _, _, action, _, _): let peerId: PeerId = chatPeerId.peerId var result = [peerId] @@ -295,7 +295,7 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere } case .messageEmpty: break - case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _): + case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _, _): if let replyHeader = replyHeader { switch replyHeader { case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset): @@ -1015,7 +1015,7 @@ extension StoreMessage { self.init(id: MessageId(peerId: peerId, namespace: namespace, id: id), globallyUniqueId: nil, groupingKey: groupingId, threadId: threadId, timestamp: date, flags: storeFlags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: forwardInfo, authorId: authorId, text: messageText, attributes: attributes, media: medias) case .messageEmpty: return nil - case let .messageService(flags, id, fromId, chatPeerId, replyTo, date, action, ttlPeriod): + case let .messageService(flags, id, fromId, chatPeerId, replyTo, date, action, reactions, ttlPeriod): let peerId: PeerId = chatPeerId.peerId let authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId @@ -1074,6 +1074,10 @@ extension StoreMessage { if (flags & (1 << 17)) != 0 { attributes.append(ContentRequiresValidationMessageAttribute()) } + + if let reactions = reactions { + attributes.append(ReactionsMessageAttribute(apiReactions: reactions)) + } var storeFlags = StoreMessageFlags() if (flags & 2) == 0 { @@ -1120,6 +1124,10 @@ extension StoreMessage { if (flags & (1 << 27)) != 0 { storeFlags.insert(.IsForumTopic) } + + if (flags & (1 << 9)) != 0 { + storeFlags.insert(.ReactionsArePossible) + } self.init(id: MessageId(peerId: peerId, namespace: namespace, id: id), globallyUniqueId: nil, groupingKey: nil, threadId: threadId, timestamp: date, flags: storeFlags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: attributes, media: media) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift index e2a3d07ac0..35d1e2aed1 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramChannel.swift @@ -24,7 +24,7 @@ public enum TelegramChannelPermission { } public extension TelegramChannel { - func hasPermission(_ permission: TelegramChannelPermission) -> Bool { + func hasPermission(_ permission: TelegramChannelPermission, ignoreDefault: Bool = false) -> Bool { if self.flags.contains(.isCreator) { if case .canBeAnonymous = permission { if let adminRights = self.adminRights { @@ -50,7 +50,7 @@ public extension TelegramChannel { if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendText) { return false } - if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendText) { + if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendText) && !ignoreDefault { return false } return true @@ -69,7 +69,7 @@ public extension TelegramChannel { if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendPhotos) { return false } - if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendPhotos) { + if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendPhotos) && !ignoreDefault { return false } return true @@ -88,7 +88,7 @@ public extension TelegramChannel { if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendVideos) { return false } - if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendVideos) { + if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendVideos) && !ignoreDefault { return false } return true @@ -121,7 +121,7 @@ public extension TelegramChannel { if let bannedRights = self.bannedRights, bannedRights.flags.intersection(flags) == flags { return false } - if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.intersection(flags) == flags { + if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.intersection(flags) == flags && !ignoreDefault { return false } return true diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 03302828e0..6246791dfb 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -108,7 +108,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes updatedTimestamp = date case .messageEmpty: break - case let .messageService(_, _, _, _, _, date, _, _): + case let .messageService(_, _, _, _, _, date, _, _, _): updatedTimestamp = date } } else { diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index cc9d8bf309..cbbbced361 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 195 + return 196 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift index f667991512..af8b1148d5 100644 --- a/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift +++ b/submodules/TelegramCore/Sources/State/UpdatesApiUtils.swift @@ -108,7 +108,7 @@ extension Api.Message { return id case let .messageEmpty(_, id, _): return id - case let .messageService(_, id, _, _, _, _, _, _): + case let .messageService(_, id, _, _, _, _, _, _, _): return id } } @@ -128,7 +128,7 @@ extension Api.Message { } else { return nil } - case let .messageService(_, id, _, chatPeerId, _, _, _, _): + case let .messageService(_, id, _, chatPeerId, _, _, _, _, _): let peerId: PeerId = chatPeerId.peerId return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id) } @@ -141,7 +141,7 @@ extension Api.Message { return peerId case let .messageEmpty(_, _, peerId): return peerId?.peerId - case let .messageService(_, _, _, chatPeerId, _, _, _, _): + case let .messageService(_, _, _, chatPeerId, _, _, _, _, _): let peerId: PeerId = chatPeerId.peerId return peerId } @@ -151,7 +151,7 @@ extension Api.Message { switch self { case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): return date - case let .messageService(_, _, _, _, _, date, _, _): + case let .messageService(_, _, _, _, _, date, _, _, _): return date case .messageEmpty: return nil diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index ba765fa57e..3cc78a261f 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -17,6 +17,7 @@ public enum ServerProvidedSuggestion: String { case todayBirthdays = "BIRTHDAY_CONTACTS_TODAY" case gracePremium = "PREMIUM_GRACE" case starsSubscriptionLowBalance = "STARS_SUBSCRIPTION_LOW_BALANCE" + case setupPhoto = "USERPIC_SETUP" } private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set]>([:]) @@ -40,7 +41,6 @@ func _internal_getServerProvidedSuggestions(account: Account) -> Signal<[ServerP guard let data = appConfiguration.data, let listItems = data["pending_suggestions"] as? [String] else { return [] } - return listItems.compactMap { item -> ServerProvidedSuggestion? in return ServerProvidedSuggestion(rawValue: item) }.filter { !dismissedSuggestions.contains($0) } diff --git a/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift b/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift index 50ca3c53a0..e58021f404 100644 --- a/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift +++ b/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift @@ -6,7 +6,7 @@ import Postbox private final class LinkHelperClass: NSObject { } -public func canSendMessagesToPeer(_ peer: Peer) -> Bool { +public func canSendMessagesToPeer(_ peer: Peer, ignoreDefault: Bool = false) -> Bool { if let peer = peer as? TelegramUser, peer.addressName == "replies" { return false } else if peer is TelegramUser || peer is TelegramGroup { @@ -14,7 +14,7 @@ public func canSendMessagesToPeer(_ peer: Peer) -> Bool { } else if let peer = peer as? TelegramSecretChat { return peer.embeddedState == .active } else if let peer = peer as? TelegramChannel { - return peer.hasPermission(.sendSomething) + return peer.hasPermission(.sendSomething, ignoreDefault: ignoreDefault) } else { return false } diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index b0d8a5351b..8d151bb7b3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -671,6 +671,8 @@ public final class ChatInlineSearchResultsListComponent: Component { editPeer: { _ in }, openWebApp: { _ in + }, + openPhotoSetup: { } ) self.chatListNodeInteraction = chatListNodeInteraction diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index 7b56c5b951..cc0cd28113 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -230,7 +230,8 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) if let _ = image { - backgroundSize.height += imageSize.height + 10 + backgroundSize.width = imageSize.width + 2.0 + backgroundSize.height += imageSize.height + 10.0 } return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in @@ -239,7 +240,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: 15.5) - let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: labelLayout.size.height + 10 + 2), size: imageSize) + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: labelLayout.size.height + 12.0), size: imageSize) if let image = image { let imageNode: TransformImageNode if let current = strongSelf.imageNode { @@ -319,7 +320,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { attemptSynchronous: synchronousLoads )) - let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: image != nil ? 2 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) + let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0) - 1.0, y: image != nil ? 2.0 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) if story != nil { let leadingIconView: UIImageView diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index b286626bdd..8fbe4f0d91 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1301,7 +1301,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { associatedData: item.associatedData, accountPeer: item.associatedData.accountPeer, isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), - constrainedWidth: maxReactionsWidth + constrainedWidth: maxReactionsWidth, + centerAligned: false )) maxContentWidth = max(maxContentWidth, minWidth) reactionButtonsFinalize = buttonsLayout diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index a2f557bdc1..43d64bc2a1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -208,6 +208,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ isAction = true if case .phoneCall = action.action { result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + needReactions = false } else if case .giftPremium = action.action { result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .giftStars = action.action { @@ -225,10 +226,16 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .joinedChannel = action.action { result.append((message, ChatMessageJoinedChannelBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + needReactions = false } else { + switch action.action { + case .photoUpdated: + break + default: + needReactions = false + } result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } - needReactions = false } else if let _ = media as? TelegramMediaMap { result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) } else if let _ = media as? TelegramMediaGame { @@ -2700,6 +2707,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))? if !bubbleReactions.reactions.isEmpty && !item.presentationData.isPreview { + var centerAligned = false + for media in item.message.media { + if media is TelegramMediaAction { + centerAligned = true + } + break + } + var maximumNodeWidth = maximumNodeWidth if hasInstantVideo { maximumNodeWidth = min(309, baseWidth - 84) @@ -2715,7 +2730,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI associatedData: item.associatedData, accountPeer: item.associatedData.accountPeer, isIncoming: incoming, - constrainedWidth: maximumNodeWidth + constrainedWidth: maximumNodeWidth, + centerAligned: centerAligned )) maxContentWidth = max(maxContentWidth, minWidth) reactionButtonsFinalize = buttonsLayout diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index efc4d58fcd..f25307df04 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -616,7 +616,8 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco associatedData: item.associatedData, accountPeer: item.associatedData.accountPeer, isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), - constrainedWidth: maxReactionsWidth + constrainedWidth: maxReactionsWidth, + centerAligned: false )) maxContentWidth = max(maxContentWidth, minWidth) reactionButtonsFinalize = buttonsLayout diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift index 690ab2d00b..be8d60aaa3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -285,7 +285,7 @@ public func canAddMessageReactions(message: Message) -> Bool { } for media in message.media { if let _ = media as? TelegramMediaAction { - return false + return message.flags.contains(.ReactionsArePossible) } else if let story = media as? TelegramMediaStory { if story.isMention { return false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift index 430dbd7bf9..38f55f1297 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode/Sources/ChatMessageReactionsFooterContentNode.swift @@ -30,6 +30,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode { public enum DisplayAlignment { case left case right + case center } private var bubbleBackgroundNode: WallpaperBubbleBackgroundNode? @@ -224,27 +225,29 @@ public final class MessageReactionButtonsNode: ASDisplayNode { constrainedWidth: constrainedWidth ) + let itemSpacing: CGFloat = 6.0 + var reactionButtonsSize = CGSize() var currentRowWidth: CGFloat = 0.0 for item in reactionButtonsResult.items { if currentRowWidth + item.size.width > constrainedWidth { reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) if !reactionButtonsSize.height.isZero { - reactionButtonsSize.height += 6.0 + reactionButtonsSize.height += itemSpacing } reactionButtonsSize.height += item.size.height currentRowWidth = 0.0 } if !currentRowWidth.isZero { - currentRowWidth += 6.0 + currentRowWidth += itemSpacing } currentRowWidth += item.size.width } if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty { reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) if !reactionButtonsSize.height.isZero { - reactionButtonsSize.height += 6.0 + reactionButtonsSize.height += itemSpacing } reactionButtonsSize.height += reactionButtonsResult.items[0].size.height } @@ -296,11 +299,14 @@ public final class MessageReactionButtonsNode: ASDisplayNode { } var reactionButtonPosition: CGPoint + switch alignment { case .left: reactionButtonPosition = CGPoint(x: -1.0, y: topInset) case .right: reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset) + case .center: + reactionButtonPosition = CGPoint(x: 0.0, y: topInset) } let reactionButtons = reactionButtonsResult.apply( @@ -312,32 +318,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode { ) var validIds = Set() - for item in reactionButtons.items { - validIds.insert(item.value) - - switch alignment { - case .left: - if reactionButtonPosition.x + item.size.width > boundingWidth { - reactionButtonPosition.x = -1.0 - reactionButtonPosition.y += item.size.height + 6.0 - } - case .right: - if reactionButtonPosition.x - item.size.width < -1.0 { - reactionButtonPosition.x = size.width + 1.0 - reactionButtonPosition.y += item.size.height + 6.0 - } - } - - let itemFrame: CGRect - switch alignment { - case .left: - itemFrame = CGRect(origin: reactionButtonPosition, size: item.size) - reactionButtonPosition.x += item.size.width + 6.0 - case .right: - itemFrame = CGRect(origin: CGPoint(x: reactionButtonPosition.x - item.size.width, y: reactionButtonPosition.y), size: item.size) - reactionButtonPosition.x -= item.size.width + 6.0 - } - + + let layoutItem: (ReactionButtonsAsyncLayoutContainer.ApplyResult.Item, CGRect) -> Void = { item, itemFrame in let itemMaskFrame = itemFrame.offsetBy(dx: backgroundInsets, dy: backgroundInsets) let itemMaskView: UIView @@ -396,6 +378,77 @@ public final class MessageReactionButtonsNode: ASDisplayNode { } } } + + if alignment == .center { + var lines: [[ReactionButtonsAsyncLayoutContainer.ApplyResult.Item]] = [] + var currentLine: [ReactionButtonsAsyncLayoutContainer.ApplyResult.Item] = [] + var currentLineWidth: CGFloat = 0.0 + + for item in reactionButtons.items { + validIds.insert(item.value) + let itemWidth = item.size.width + + if currentLineWidth + itemWidth + (currentLine.isEmpty ? 0 : itemSpacing) > boundingWidth + itemSpacing { + lines.append(currentLine) + currentLine = [item] + currentLineWidth = itemWidth + } else { + currentLine.append(item) + currentLineWidth += (currentLine.isEmpty ? 0 : itemSpacing) + itemWidth + } + } + if !currentLine.isEmpty { + lines.append(currentLine) + } + + var yPosition = topInset + + for line in lines { + let totalItemWidth = line.reduce(0.0) { $0 + $1.size.width } + CGFloat(line.count - 1) * itemSpacing + let startX = (boundingWidth - totalItemWidth) / 2.0 + var xPosition = startX + + for item in line { + let itemFrame = CGRect(origin: CGPoint(x: xPosition, y: yPosition), size: item.size) + xPosition += item.size.width + itemSpacing + + layoutItem(item, itemFrame) + } + yPosition += line.first!.size.height + itemSpacing + } + } else { + for item in reactionButtons.items { + validIds.insert(item.value) + + switch alignment { + case .left: + if reactionButtonPosition.x + item.size.width > boundingWidth { + reactionButtonPosition.x = -1.0 + reactionButtonPosition.y += item.size.height + itemSpacing + } + case .right: + if reactionButtonPosition.x - item.size.width < -1.0 { + reactionButtonPosition.x = size.width + 1.0 + reactionButtonPosition.y += item.size.height + itemSpacing + } + default: + break + } + + let itemFrame: CGRect + switch alignment { + case .left, .center: + itemFrame = CGRect(origin: reactionButtonPosition, size: item.size) + reactionButtonPosition.x += item.size.width + itemSpacing + case .right: + itemFrame = CGRect(origin: CGPoint(x: reactionButtonPosition.x - item.size.width, y: reactionButtonPosition.y), size: item.size) + reactionButtonPosition.x -= item.size.width + itemSpacing + } + + + layoutItem(item, itemFrame) + } + } var removeMaskIds: [MessageReaction.Reaction] = [] for (id, view) in strongSelf.backgroundMaskButtons { @@ -629,6 +682,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode { public let accountPeer: EnginePeer? public let isIncoming: Bool public let constrainedWidth: CGFloat + public let centerAligned: Bool public init( context: AccountContext, @@ -641,7 +695,8 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode { associatedData: ChatMessageItemAssociatedData, accountPeer: EnginePeer?, isIncoming: Bool, - constrainedWidth: CGFloat + constrainedWidth: CGFloat, + centerAligned: Bool ) { self.context = context self.presentationData = presentationData @@ -654,6 +709,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode { self.accountPeer = accountPeer self.isIncoming = isIncoming self.constrainedWidth = constrainedWidth + self.centerAligned = centerAligned } } @@ -682,6 +738,13 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode { return { arguments in let node = maybeNode ?? ChatMessageReactionButtonsNode() + let alignment: MessageReactionButtonsNode.DisplayAlignment + if arguments.centerAligned { + alignment = .center + } else { + alignment = arguments.isIncoming ? .left : .right + } + let buttonsUpdate = node.buttonsNode.prepareUpdate( context: arguments.context, presentationData: arguments.presentationData, @@ -692,7 +755,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode { accountPeer: arguments.accountPeer, message: arguments.message, associatedData: arguments.associatedData, - alignment: arguments.isIncoming ? .left : .right, + alignment: alignment, constrainedWidth: arguments.constrainedWidth, type: .freeform ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift index fd68944f0a..3dc0a1b635 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift @@ -432,8 +432,8 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { buttons = [ self.deleteButton, tagButton, - self.forwardButton, - self.shareButton + self.shareButton, + self.forwardButton ] } else { buttons = [ diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index c6c2ccdcc1..c968aa1a50 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -863,7 +863,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { associatedData: item.associatedData, accountPeer: item.associatedData.accountPeer, isIncoming: item.message.effectivelyIncoming(item.context.account.peerId), - constrainedWidth: maxReactionsWidth + constrainedWidth: maxReactionsWidth, + centerAligned: false )) maxContentWidth = max(maxContentWidth, minWidth) reactionButtonsFinalize = buttonsLayout diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index ca2b0a1223..01af7aae04 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -631,6 +631,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in + }, sendGift: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index ce99d5b1f8..57af149086 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -488,6 +488,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in + }, sendGift: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index fa52fd8c41..b073853d4d 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -267,6 +267,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openAgeRestrictedMessageMedia: (Message, @escaping () -> Void) -> Void public let playMessageEffect: (Message) -> Void public let editMessageFactCheck: (MessageId) -> Void + public let sendGift: (EnginePeer.Id) -> Void public let requestMessageUpdate: (MessageId, Bool) -> Void public let cancelInteractiveKeyboardGestures: () -> Void @@ -400,6 +401,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openAgeRestrictedMessageMedia: @escaping (Message, @escaping () -> Void) -> Void, playMessageEffect: @escaping (Message) -> Void, editMessageFactCheck: @escaping (MessageId) -> Void, + sendGift: @escaping (EnginePeer.Id) -> Void, requestMessageUpdate: @escaping (MessageId, Bool) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, dismissTextInput: @escaping () -> Void, @@ -512,6 +514,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openAgeRestrictedMessageMedia = openAgeRestrictedMessageMedia self.playMessageEffect = playMessageEffect self.editMessageFactCheck = editMessageFactCheck + self.sendGift = sendGift self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 652eddf8b2..5912f584c5 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -406,7 +406,7 @@ final class GiftSetupScreenComponent: Component { purpose: .starGift(peerId: component.peerId, requiredStars: starGift.price), completion: { [weak starsContext] stars in starsContext?.add(balance: StarsAmount(value: stars, nanos: 0)) - Queue.mainQueue().after(0.1) { + Queue.mainQueue().after(2.0) { proceed() } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index b6fbb079cc..adabdfc782 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -186,6 +186,7 @@ public final class LoadingOverlayNode: ASDisplayNode { }, dismissNotice: { _ in }, editPeer: { _ in }, openWebApp: { _ in + }, openPhotoSetup: { }) let items = (0 ..< 1).map { _ -> ChatListItem in @@ -542,6 +543,8 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod editPeer: { _ in }, openWebApp: { _ in + }, + openPhotoSetup: { } ) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index d07271a61f..d51aea4c8e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3621,6 +3621,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in + }, sendGift: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { @@ -9788,7 +9789,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } })) } - + fileprivate func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) { guard let peer = self.data?.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: self.data?.threadData) else { return @@ -9825,7 +9826,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro legacyController.bind(controller: navigationController) strongSelf.view.endEditing(true) - (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.present(legacyController, in: .window(.root)) + + let parentController = (strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController + + parentController?.present(legacyController, in: .window(.root)) var hasPhotos = false if !peer.profileImageRepresentations.isEmpty { @@ -9876,7 +9880,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasDeleteButton, hasViewButton: false, personalPhoto: strongSelf.isSettings || strongSelf.isMyProfile, isVideo: currentIsVideo, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: isForum, title: title, isSuggesting: [.custom, .suggest].contains(mode))! mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context) let _ = strongSelf.currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { [weak self] assetsController in + mixin.requestSearchController = { [weak self, weak parentController] assetsController in guard let strongSelf = self else { return } @@ -9885,14 +9889,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self?.updateProfilePhoto(result, mode: mode) })) controller.navigationPresentation = .modal - (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller) + parentController?.push(controller) if fromGallery { completion(nil) } } var isFromEditor = false - mixin.requestAvatarEditor = { [weak self] imageCompletion, videoCompletion in + mixin.requestAvatarEditor = { [weak self, weak parentController] imageCompletion, videoCompletion in guard let strongSelf = self, let imageCompletion, let videoCompletion else { return } @@ -9913,27 +9917,33 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let controller = AvatarEditorScreen(context: strongSelf.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup) controller.imageCompletion = imageCompletion controller.videoCompletion = videoCompletion - (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller) + parentController?.push(controller) isFromEditor = true + + Queue.mainQueue().after(1.0) { + if let rootController = strongSelf.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.openSettings() + } + } } if let confirmationTextPhoto, let confirmationAction { - mixin.willFinishWithImage = { [weak self] image, commit in + mixin.willFinishWithImage = { [weak self, weak parentController] image, commit in if let strongSelf = self, let image { let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextPhoto, doneTitle: confirmationAction, commit: { commit?() }) - (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.presentInGlobalOverlay(controller) + parentController?.presentInGlobalOverlay(controller) } } } if let confirmationTextVideo, let confirmationAction { - mixin.willFinishWithVideo = { [weak self] image, commit in + mixin.willFinishWithVideo = { [weak self, weak parentController] image, commit in if let strongSelf = self, let image { let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextVideo, doneTitle: confirmationAction, isDark: !isFromEditor, commit: { commit?() }) - (strongSelf.controller?.navigationController?.topViewController as? ViewController)?.presentInGlobalOverlay(controller) + parentController?.presentInGlobalOverlay(controller) } } } @@ -13008,6 +13018,20 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } } + public func openAvatarSetup() { + let proceed = { [weak self] in + self?.controllerNode.openAvatarForEditing() + } + if !self.isNodeLoaded { + self.loadDisplayNode() + Queue.mainQueue().after(0.1) { + proceed() + } + } else { + proceed() + } + } + public func updateProfilePhoto(_ image: UIImage, mode: PeerInfoAvatarEditingMode) { if !self.isNodeLoaded { self.loadDisplayNode() diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 75df903a85..5178bb3662 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -209,6 +209,8 @@ final class GreetingMessageListItemComponent: Component { editPeer: { _ in }, openWebApp: { _ in + }, + openPhotoSetup: { } ) self.chatListNodeInteraction = chatListNodeInteraction diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 37c5b4caa0..7bd38b6ac1 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -230,6 +230,8 @@ final class QuickReplySetupScreenComponent: Component { } }, openWebApp: { _ in + }, + openPhotoSetup: { } ) diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index ec687b8b91..77af1366a9 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -874,6 +874,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate }, dismissNotice: { _ in }, editPeer: { _ in }, openWebApp: { _ in + }, openPhotoSetup: { }) let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/CameraIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Avatar/CameraIcon.imageset/Contents.json new file mode 100644 index 0000000000..cd01ba2cd8 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Avatar/CameraIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "setavatar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/CameraIcon.imageset/setavatar.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/CameraIcon.imageset/setavatar.pdf new file mode 100644 index 0000000000..0079fd0c99 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Avatar/CameraIcon.imageset/setavatar.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 83ff745532..881792d520 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2093,7 +2093,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, peer.hasBannedPermission(.banSendStickers) != nil { - if let boostsToUnrestrict = strongSelf.presentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0, (strongSelf.presentationInterfaceState.appliedBoosts ?? 0) < boostsToUnrestrict { + if !canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState) { strongSelf.interfaceInteraction?.openBoostToUnrestrict() return false } @@ -2245,7 +2245,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, peer.hasBannedPermission(.banSendGifs) != nil { - if let boostsToUnrestrict = strongSelf.presentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0, (strongSelf.presentationInterfaceState.appliedBoosts ?? 0) < boostsToUnrestrict { + if !canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState) { strongSelf.interfaceInteraction?.openBoostToUnrestrict() return false } @@ -2296,7 +2296,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, peer.hasBannedPermission(.banSendGifs) != nil { - if let boostsToUnrestrict = strongSelf.presentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0, (strongSelf.presentationInterfaceState.appliedBoosts ?? 0) < boostsToUnrestrict { + if !canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState) { strongSelf.interfaceInteraction?.openBoostToUnrestrict() return false } @@ -4554,6 +4554,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } self.openEditMessageFactCheck(messageId: messageId) + }, sendGift: { [weak self] peerId in + guard let self else { + return + } + let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) + |> filter { !$0.isEmpty } + |> deliverOnMainQueue).start(next: { [weak self] giftOptions in + guard let self else { + return + } + let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let controller = self.context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: false) + self.push(controller) + }) }, requestMessageUpdate: { [weak self] id, scroll in if let self { self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 72889de0d1..96546f93c8 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -339,7 +339,8 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS case .peer: if let channel = peer as? TelegramChannel { if case .member = channel.participationStatus { - canReply = channel.hasPermission(.sendSomething) + let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: chatPresentationInterfaceState) + canReply = channel.hasPermission(.sendSomething, ignoreDefault: canBypassRestrictions) } if case .broadcast = channel.info { canReply = true @@ -1120,6 +1121,23 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + if data.messageActions.options.contains(.sendGift) { + //TODO:localize + let sendGiftTitle: String + if message.effectivelyIncoming(context.account.peerId) { + let peerName = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle ?? "" + sendGiftTitle = "Send Gift to \(peerName)" + } else { + sendGiftTitle = "Send Another Gift" + } + actions.append(.action(ContextMenuActionItem(text: sendGiftTitle, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + let _ = controllerInteraction.sendGift(message.id.peerId) + f(.dismissWithoutContent) + }))) + } + var isReplyThreadHead = false if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { isReplyThreadHead = messages[0].id == replyThreadMessage.effectiveTopId @@ -2220,8 +2238,15 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer } break } - } else if let action = media as? TelegramMediaAction, case .phoneCall = action.action { - optionsMap[id]!.insert(.rateCall) + } else if let action = media as? TelegramMediaAction { + switch action.action { + case .phoneCall: + optionsMap[id]!.insert(.rateCall) + case .starGift: + optionsMap[id]!.insert(.sendGift) + default: + break + } } else if let story = media as? TelegramMediaStory { if let story = message.associatedStories[story.storyId], story.data.isEmpty { isShareProtected = true diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index c81bd6ea28..57468824f2 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -9,16 +9,6 @@ import ChatBotStartInputPanelNode import ChatChannelSubscriberInputPanelNode import ChatMessageSelectionInputPanelNode -func canBypassRestrictions(chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool { - guard let boostsToUnrestrict = chatPresentationInterfaceState.boostsToUnrestrict else { - return false - } - if let appliedBoosts = chatPresentationInterfaceState.appliedBoosts, appliedBoosts >= boostsToUnrestrict { - return true - } - return false -} - func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) { if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil { return (nil, nil) diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index 9c6b8d4c23..bd04b63d7f 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -294,6 +294,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe }, dismissNotice: { _ in }, editPeer: { _ in }, openWebApp: { _ in + }, openPhotoSetup: { }) interaction.searchTextHighightState = searchQuery self.interaction = interaction diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index 16c6325453..a8fb9d2c87 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -179,6 +179,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { editPeer: { _ in }, openWebApp: { _ in + }, + openPhotoSetup: { } ) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index f51c17d359..29951f6d65 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -180,6 +180,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in + }, sendGift: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e43cf24cba..814939de0f 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1796,6 +1796,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in + }, sendGift: { _ in }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 8b285bf426..95a0038bfd 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -744,6 +744,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon public func openBirthdaySetup() { self.accountSettingsController?.openBirthdaySetup() } + + public func openPhotoSetup() { + self.accountSettingsController?.openAvatarSetup() + } } //Xcode 16