diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index ed86535e8b..c8f2a8c8b2 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -159,7 +159,7 @@ "moduleExtensions": { "@@apple_support+//crosstool:setup.bzl%apple_cc_configure_extension": { "general": { - "bzlTransitiveDigest": "RjubjYIojbv0PxTpnoknalV9QzT9asbV7elDuN7m2A4=", + "bzlTransitiveDigest": "xcBTf2+GaloFpg7YEh/Bv+1yAczRkiCt3DGws4K7kSk=", "usagesDigest": "lfcV4HxPD+NLaRIT/v7BtSGFgE7c9xrWU7jDiwBAxzo=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 12c3f48fa5..4525e1e69c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -666,14 +666,15 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } else { authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" } - if isSelfGift { - title = isStoryEntity ? uniqueGift.title : item.presentationData.strings.Notification_StarGift_Self_Title + if isStoryEntity { + title = uniqueGift.title + } else if isSelfGift { + title = item.presentationData.strings.Notification_StarGift_Self_Title } else if item.message.id.peerId.isTelegramNotifications { title = item.presentationData.strings.Notification_StarGift_TitleShort } else { - title = isStoryEntity ? uniqueGift.title : item.presentationData.strings.Notification_StarGift_Title(authorName).string - } - + title = item.presentationData.strings.Notification_StarGift_Title(authorName).string + } text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" : "**\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index c9fdf2a450..90a8a1ae83 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -392,8 +392,7 @@ final class GiftOptionsScreenComponent: Component { color: .blue ) } - - if gift.flags.contains(.requiresPremium) { + if !isSoldOut && gift.flags.contains(.requiresPremium) { ribbon = GiftItemComponent.Ribbon( text: environment.strings.Gift_Options_Gift_Premium, color: .orange diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index d4bce4a186..775f7a183f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -36,14 +36,14 @@ private final class GiftValueSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let gift: ProfileGiftsContext.State.StarGift + let gift: StarGift let valueInfo: StarGift.UniqueGift.ValueInfo let animateOut: ActionSlot> let getController: () -> ViewController? init( context: AccountContext, - gift: ProfileGiftsContext.State.StarGift, + gift: StarGift, valueInfo: StarGift.UniqueGift.ValueInfo, animateOut: ActionSlot>, getController: @escaping () -> ViewController? @@ -219,7 +219,7 @@ private final class GiftValueSheetContent: CombinedComponent { var giftIconSubject: GiftItemComponent.Subject? var genericGift: StarGift.Gift? - switch component.gift.gift { + switch component.gift { case let .generic(gift): animationFile = gift.file giftIconSubject = .starGift(gift: gift, price: "") @@ -676,12 +676,12 @@ final class GiftValueSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let gift: ProfileGiftsContext.State.StarGift + let gift: StarGift let valueInfo: StarGift.UniqueGift.ValueInfo init( context: AccountContext, - gift: ProfileGiftsContext.State.StarGift, + gift: StarGift, valueInfo: StarGift.UniqueGift.ValueInfo ) { self.context = context @@ -796,12 +796,12 @@ final class GiftValueSheetComponent: CombinedComponent { final class GiftValueScreen: ViewControllerComponentContainer { private let context: AccountContext - private let gift: ProfileGiftsContext.State.StarGift + private let gift: StarGift private let valueInfo: StarGift.UniqueGift.ValueInfo public init( context: AccountContext, - gift: ProfileGiftsContext.State.StarGift, + gift: StarGift, valueInfo: StarGift.UniqueGift.ValueInfo ) { self.context = context diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 161a2473f3..503bdb7141 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -633,7 +633,7 @@ private final class GiftViewSheetContent: CombinedComponent { } func openValue() { - guard let controller = self.getController(), case let .profileGift(_, gift) = self.subject, case let .unique(uniqueGift) = gift.gift else { + guard let controller = self.getController(), let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift else { return } let _ = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: uniqueGift.slug) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift index 5d1481f5c1..32e3928b92 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -23,6 +23,50 @@ public enum PeerInfoPaneKey: Int32 { case groupsInCommon case similarChannels case similarBots + + public init(tab: TelegramProfileTab) { + switch tab { + case .files: + self = .files + case .gifs: + self = .gifs + case .gifts: + self = .gifts + case .links: + self = .links + case .media: + self = .media + case .music: + self = .music + case .posts: + self = .stories + case .voice: + self = .voice + } + } + + public var tab: TelegramProfileTab? { + switch self { + case .stories: + return .posts + case .gifts: + return .gifts + case .media: + return .media + case .files: + return .files + case .music: + return .music + case .voice: + return .voice + case .links: + return .links + case .gifs: + return .gifs + default: + return nil + } + } } public struct PeerInfoStatusData: Equatable { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index e844a6c72e..6096e4a327 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -161,6 +161,9 @@ swift_library( "//submodules/Components/HierarchyTrackingLayer", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent", "//submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen", + "//submodules/TelegramUI/Components/TabSelectorComponent", + "//submodules/TelegramUI/Components/BottomButtonPanelComponent", + "//submodules/TelegramUI/Components/MarqueeComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 61e7d41580..dba35f006c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -1320,9 +1320,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags = .single(false) } - let starsRevenueContextAndState = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> mapToSignal { peer -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in - var canViewStarsRevenue = false + let starsRevenueContextAndState = combineLatest( + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> distinctUntilChanged, + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId)) + |> distinctUntilChanged + ) + |> mapToSignal { peer, canViewRevenue -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in + var canViewStarsRevenue = canViewRevenue if let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal || context.sharedContext.immediateExperimentalUISettings.devRequests { canViewStarsRevenue = true } @@ -1399,14 +1404,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) - if let hasStoryArchive, hasStoryArchive { - availablePanes?.insert(.storyArchive, at: 1) - } if availablePanes != nil, profileGiftsContext != nil, let cachedData = peerView.cachedData as? CachedUserData { if let starGiftsCount = cachedData.starGiftsCount, starGiftsCount > 0 { - availablePanes?.insert(.gifts, at: hasStoryArchive == true ? 2 : 1) + availablePanes?.insert(.gifts, at: 1) } } + if let hasStoryArchive, hasStoryArchive { + availablePanes?.append(.storyArchive) + } } else if let hasStories { if hasStories, peerView.peers[peerView.peerId] is TelegramUser, peerView.peerId != context.account.peerId { availablePanes?.insert(.stories, at: 0) @@ -1454,6 +1459,15 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes = nil } + if var currentAvailablePanes = availablePanes, let cachedData = peerView.cachedData as? CachedUserData, let mainProfileTab = cachedData.mainProfileTab { + let mainTabKey = PeerInfoPaneKey(tab: mainProfileTab) + if currentAvailablePanes.contains(mainTabKey) && currentAvailablePanes.first != mainTabKey { + currentAvailablePanes = currentAvailablePanes.filter { $0 != mainTabKey } + currentAvailablePanes.insert(mainTabKey, at: 0) + availablePanes = currentAvailablePanes + } + } + let peer = peerView.peers[userPeerId] var globalSettings: TelegramGlobalSettings? @@ -1686,6 +1700,15 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen availablePanes = nil } + if var currentAvailablePanes = availablePanes, let cachedData = peerView.cachedData as? CachedChannelData, let mainProfileTab = cachedData.mainProfileTab { + let mainTabKey = PeerInfoPaneKey(tab: mainProfileTab) + if currentAvailablePanes.contains(mainTabKey) && currentAvailablePanes.first != mainTabKey { + currentAvailablePanes = currentAvailablePanes.filter { $0 != mainTabKey } + currentAvailablePanes.insert(mainTabKey, at: 0) + availablePanes = currentAvailablePanes + } + } + var discussionPeer: Peer? if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { discussionPeer = peer diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 97512e4a65..15eb470c1e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -15,6 +15,11 @@ import PeerInfoChatListPaneNode import PeerInfoChatPaneNode import TextFormat import EmojiTextAttachmentView +import ComponentFlow +import TabSelectorComponent +import MultilineTextComponent +import BottomButtonPanelComponent +import UndoUI final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey @@ -38,6 +43,176 @@ final class PeerInfoPaneWrapper { } } +private final class GiftsTabItemComponent: Component { + typealias EnvironmentType = TabSelectorComponent.ItemEnvironment + + let context: AccountContext + let icons: [ProfileGiftsContext.State.StarGift] + let title: String + let theme: PresentationTheme + + init( + context: AccountContext, + icons: [ProfileGiftsContext.State.StarGift], + title: String, + theme: PresentationTheme + ) { + self.context = context + self.icons = icons + self.title = title + self.theme = theme + } + + static func ==(lhs: GiftsTabItemComponent, rhs: GiftsTabItemComponent) -> Bool { + if lhs.icons != rhs.icons { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.theme !== rhs.theme { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + private let icon = ComponentView() + private var iconLayers: [AnyHashable: InlineStickerItemLayer] = [:] + + private var component: GiftsTabItemComponent? + + func update(component: GiftsTabItemComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let environment = environment[EnvironmentType.self].value + + let textSpacing: CGFloat = 2.0 + let iconSpacing: CGFloat = 1.0 + + let normalColor = component.theme.list.itemSecondaryTextColor + let selectedColor = component.theme.list.itemAccentColor + let effectiveColor = normalColor.mixedWith(selectedColor, alpha: environment.selectionFraction) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.medium(14.0), textColor: effectiveColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + + var iconOffset: CGFloat = titleSize.width + textSpacing + var iconsWidth: CGFloat = 0.0 + if !component.icons.isEmpty { + iconsWidth += iconSpacing + var validIds = Set() + var index = 0 + for icon in component.icons { + let id: AnyHashable + if let reference = icon.reference { + id = reference + } else { + id = index + } + validIds.insert(id) + + let iconSize = CGSize(width: 18.0, height: 18.0) + let animationLayer: InlineStickerItemLayer + if let current = self.iconLayers[id] { + animationLayer = current + } else { + var file: TelegramMediaFile? + switch icon.gift { + case let .generic(gift): + file = gift.file + case let .unique(gift): + for attribute in gift.attributes { + if case let .model(_, fileValue, _) = attribute { + file = fileValue + } + } + } + guard let file else { + continue + } + + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: file.fileId.id, + file: file + ) + animationLayer = InlineStickerItemLayer( + context: .account(component.context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: file, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + unique: true, + placeholderColor: component.theme.list.mediaPlaceholderColor, + pointSize: iconSize, + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + self.iconLayers[id] = animationLayer + self.layer.addSublayer(animationLayer) + + animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + } + transition.setFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: iconOffset, y: 0.0), size: iconSize)) + iconOffset += iconSize.width + iconSpacing + iconsWidth += iconSize.width + iconSpacing + + index += 1 + } + + var removeIds: [AnyHashable] = [] + for (id, layer) in self.iconLayers { + if !validIds.contains(id) { + removeIds.append(id) + layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + layer.removeFromSuperlayer() + }) + } + } + for id in removeIds { + self.iconLayers.removeValue(forKey: id) + } + } else { + for (_, layer) in self.iconLayers { + layer.removeFromSuperlayer() + } + self.iconLayers.removeAll() + } + + let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + return CGSize(width: titleSize.width + iconsWidth, height: titleSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { private let pressed: () -> Void @@ -702,8 +877,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat private let coveringBackgroundNode: NavigationBackgroundNode private let additionalBackgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode - private let tabsContainerNode: PeerInfoPaneTabsContainerNode + private let tabsContainer = ComponentView() private let tabsSeparatorNode: ASDisplayNode + private var didJustReorderTabs = false + + private var actionPanel: ComponentView? let isReady = Promise() var didSetIsReady = false @@ -780,52 +958,15 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat self.coveringBackgroundNode = NavigationBackgroundNode(color: .clear) self.coveringBackgroundNode.isUserInteractionEnabled = false - - self.tabsContainerNode = PeerInfoPaneTabsContainerNode(context: context) - + self.tabsSeparatorNode = ASDisplayNode() - self.tabsSeparatorNode.isLayerBacked = true super.init() // self.addSubnode(self.separatorNode) self.addSubnode(self.additionalBackgroundNode) self.addSubnode(self.coveringBackgroundNode) - self.addSubnode(self.tabsContainerNode) self.addSubnode(self.tabsSeparatorNode) - - self.tabsContainerNode.requestSelectPane = { [weak self] key in - guard let strongSelf = self else { - return - } - if strongSelf.currentPaneKey == key { - if let requestExpandTabs = strongSelf.requestExpandTabs, requestExpandTabs() { - } else { - let _ = strongSelf.currentPane?.node.scrollToTop() - } - return - } - if strongSelf.currentPanes[key] != nil { - strongSelf.currentPaneKey = key - - if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) - - strongSelf.currentPaneUpdated?(true) - - strongSelf.currentPaneStatusPromise.set(strongSelf.currentPane?.node.status ?? .single(nil)) - strongSelf.nextPaneStatusPromise.set(.single(nil)) - strongSelf.paneTransitionPromise.set(nil) - } - } else if strongSelf.pendingSwitchToPaneKey != key { - strongSelf.pendingSwitchToPaneKey = key - strongSelf.expandOnSwitch = true - - if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) - } - } - } } override func didLoad() { @@ -844,7 +985,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat guard let currentPaneKey = strongSelf.currentPaneKey, let availablePanes = currentParams.data?.availablePanes, let index = availablePanes.firstIndex(of: currentPaneKey) else { return [] } - if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) { + if let tabsContainerView = strongSelf.tabsContainer.view, tabsContainerView.bounds.contains(strongSelf.view.convert(point, to: tabsContainerView)) { return [] } if case .savedMessagesChats = currentPaneKey { @@ -1017,6 +1158,47 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat } } + func openTabContextMenu(key: PeerInfoPaneKey, sourceNode: ASDisplayNode, gesture: ContextGesture?) { + guard let params = self.currentParams, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else { + return + } + + //TODO:localize + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: "Set as Main Tab", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.actionSheet.primaryTextColor) + }, action: { [weak self] _, f in + guard let self else { + return + } + f(.default) + + guard let tab = key.tab else { + return + } + Queue.mainQueue().after(0.15) { + self.didJustReorderTabs = true + let _ = (self.context.engine.peers.setMainProfileTab(peerId: self.peerId, tab: tab) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self else { + return + } + let controller = UndoOverlayController(presentationData: params.presentationData, content: .actionSucceeded(title: nil, text: "Tab order changed.", cancel: nil, destructive: false), action: { _ in return true }) + self.parentController?.present(controller, in: .current) + }) + } + }))) + + let contextController = ContextController( + presentationData: params.presentationData, + source: .extracted(TabsExtractedContentSource(sourceNode: sourceNode)), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: gesture + ) + self.parentController?.presentInGlobalOverlay(contextController) + } + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, areTabsHidden: Bool, disableTabSwitching: Bool, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { let previousAvailablePanes = self.currentAvailablePanes let availablePanes = data?.availablePanes ?? [] @@ -1240,7 +1422,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat paneDefaultTransition = .immediate } - + + if self.didJustReorderTabs && previousAvailablePanes != availablePanes { + self.didJustReorderTabs = false + paneDefaultTransition = .immediate + } + if let _ = data { if let previousAvailablePanes = previousAvailablePanes, previousAvailablePanes.isEmpty, !availablePanes.isEmpty { self.shouldFadeIn = true @@ -1323,59 +1510,145 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat tabsAlpha = 1.0 - tabsOffset / tabsHeight } tabsAlpha *= tabsAlpha - transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: sideInset, y: -tabsOffset), size: CGSize(width: size.width - sideInset * 2.0, height: tabsHeight))) - transition.updateAlpha(node: self.tabsContainerNode, alpha: tabsAlpha) - + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) transition.updateFrame(node: self.additionalBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) self.coveringBackgroundNode.update(size: self.coveringBackgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) - - self.tabsContainerNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in - let title: String - var icons: [ProfileGiftsContext.State.StarGift] = [] + + var canManageTabs = false + if let peer = data?.peer { + if peer.id == self.context.account.peerId { + canManageTabs = true + } else if let channel = data?.peer as? TelegramChannel, case .broadcast = channel.info { + if channel.hasPermission(.changeInfo) { + canManageTabs = true + } + } + } + + let items: [TabSelectorComponent.Item] = availablePanes.map { key in + let content: TabSelectorComponent.Item.Content + var canReorder = false switch key { case .stories: - title = presentationData.strings.PeerInfo_PaneStories + content = .text(presentationData.strings.PeerInfo_PaneStories) + canReorder = true case .storyArchive: - title = presentationData.strings.PeerInfo_PaneArchivedStories + content = .text(presentationData.strings.PeerInfo_PaneArchivedStories) case .botPreview: - title = presentationData.strings.PeerInfo_PaneBotPreviews + content = .text(presentationData.strings.PeerInfo_PaneBotPreviews) case .media: - title = presentationData.strings.PeerInfo_PaneMedia + content = .text(presentationData.strings.PeerInfo_PaneMedia) + canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .files: - title = presentationData.strings.PeerInfo_PaneFiles + content = .text(presentationData.strings.PeerInfo_PaneFiles) + canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .links: - title = presentationData.strings.PeerInfo_PaneLinks + content = .text(presentationData.strings.PeerInfo_PaneLinks) + canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .voice: - title = presentationData.strings.PeerInfo_PaneVoiceAndVideo + content = .text(presentationData.strings.PeerInfo_PaneVoiceAndVideo) + canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .gifs: - title = presentationData.strings.PeerInfo_PaneGifs + content = .text(presentationData.strings.PeerInfo_PaneGifs) + canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .music: - title = presentationData.strings.PeerInfo_PaneAudio + content = .text(presentationData.strings.PeerInfo_PaneAudio) + canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .groupsInCommon: - title = presentationData.strings.PeerInfo_PaneGroups + content = .text(presentationData.strings.PeerInfo_PaneGroups) case .members: - title = presentationData.strings.PeerInfo_PaneMembers + content = .text(presentationData.strings.PeerInfo_PaneMembers) case .similarChannels: - title = presentationData.strings.PeerInfo_PaneRecommended + content = .text(presentationData.strings.PeerInfo_PaneRecommended) case .similarBots: - title = presentationData.strings.PeerInfo_PaneRecommendedBots + content = .text(presentationData.strings.PeerInfo_PaneRecommendedBots) case .savedMessagesChats: - title = presentationData.strings.DialogList_TabTitle + content = .text(presentationData.strings.DialogList_TabTitle) case .savedMessages: - title = presentationData.strings.PeerInfo_SavedMessagesTabTitle + content = .text(presentationData.strings.PeerInfo_SavedMessagesTabTitle) case .gifts: - title = presentationData.strings.PeerInfo_PaneGifts + var icons: [ProfileGiftsContext.State.StarGift] = [] if let gifts = data?.profileGiftsContext?.currentState?.gifts.prefix(3) { icons = Array(gifts) } + content = .component(AnyComponent( + GiftsTabItemComponent(context: self.context, icons: icons, title: presentationData.strings.PeerInfo_PaneGifts, theme: presentationData.theme) + )) + canReorder = true } - return PeerInfoPaneSpecifier(key: key, title: title, icons: icons) - }, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition) + return TabSelectorComponent.Item(id: key, content: content, isReorderable: false, contextAction: key != availablePanes.first && canManageTabs && canReorder ? { [weak self] node, gesture in + self?.openTabContextMenu(key: key, sourceNode: node, gesture: gesture) + } : nil) + } + let tabsContainerSize = CGSize(width: size.width - sideInset * 2.0, height: tabsHeight) + let tabsContainerEffectiveSize = self.tabsContainer.update( + transition: ComponentTransition(transition), + component: AnyComponent(TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: presentationData.theme.list.itemSecondaryTextColor, + selection: presentationData.theme.list.itemAccentColor + ), + theme: presentationData.theme, + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: 6.0, + fillWidth: true, + lineSelection: true + ), + items: items, + selectedId: self.currentPaneKey, + setSelectedId: { [weak self] id in + guard let strongSelf = self, let key = id.base as? PeerInfoPaneKey else { + return + } + if strongSelf.currentPaneKey == key { + if let requestExpandTabs = strongSelf.requestExpandTabs, requestExpandTabs() { + } else { + let _ = strongSelf.currentPane?.node.scrollToTop() + } + return + } + if strongSelf.currentPanes[key] != nil { + strongSelf.currentPaneKey = key + + if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) + + strongSelf.currentPaneUpdated?(true) + + strongSelf.currentPaneStatusPromise.set(strongSelf.currentPane?.node.status ?? .single(nil)) + strongSelf.nextPaneStatusPromise.set(.single(nil)) + strongSelf.paneTransitionPromise.set(nil) + } + } else if strongSelf.pendingSwitchToPaneKey != key { + strongSelf.pendingSwitchToPaneKey = key + strongSelf.expandOnSwitch = true + + if let (size, sideInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) + } + } + }, + transitionFraction: -self.transitionFraction + )), + environment: {}, + containerSize: tabsContainerSize + ) + let tabContainerFrameOriginX = floorToScreenPixels((size.width - tabsContainerEffectiveSize.width) / 2.0) + let tabContainerFrame = CGRect(origin: CGPoint(x: tabContainerFrameOriginX, y: 10.0 - tabsOffset), size: tabsContainerSize) + if let tabsContainerView = self.tabsContainer.view { + if tabsContainerView.superview == nil { + self.view.insertSubview(tabsContainerView, belowSubview: self.tabsSeparatorNode.view) + } + transition.updateFrame(view: tabsContainerView, frame: tabContainerFrame) + transition.updateAlpha(layer: tabsContainerView.layer, alpha: tabsAlpha) + } + for (_, pane) in self.pendingPanes { let paneTransition: ContainedViewLayoutTransition = .immediate paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) @@ -1425,3 +1698,23 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat } } } + +private final class TabsExtractedContentSource: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = true + + private let sourceNode: ContextExtractedContentContainingNode + + init(sourceNode: ContextExtractedContentContainingNode) { + self.sourceNode = sourceNode + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index ff20654b6b..ad5852a1aa 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -53,14 +53,16 @@ public final class TabSelectorComponent: Component { public var font: UIFont public var spacing: CGFloat public var innerSpacing: CGFloat? + public var fillWidth: Bool public var lineSelection: Bool public var verticalInset: CGFloat public var allowScroll: Bool - public init(font: UIFont, spacing: CGFloat, innerSpacing: CGFloat? = nil, lineSelection: Bool = false, verticalInset: CGFloat = 0.0, allowScroll: Bool = true) { + public init(font: UIFont, spacing: CGFloat, innerSpacing: CGFloat? = nil, fillWidth: Bool = false, lineSelection: Bool = false, verticalInset: CGFloat = 0.0, allowScroll: Bool = true) { self.font = font self.spacing = spacing self.innerSpacing = innerSpacing + self.fillWidth = fillWidth self.lineSelection = lineSelection self.verticalInset = verticalInset self.allowScroll = allowScroll @@ -630,7 +632,10 @@ public final class TabSelectorComponent: Component { } let estimatedContentWidth = 2.0 * spacing + innerContentWidth + (CGFloat(component.items.count - 1) * (spacing + innerInset)) - if estimatedContentWidth > availableSize.width && !allowScroll { + if component.customLayout?.fillWidth == true && estimatedContentWidth < availableSize.width { + spacing = (availableSize.width - innerContentWidth) / CGFloat(component.items.count + 1) + innerInset = 0.0 + } else if estimatedContentWidth > availableSize.width && !allowScroll { spacing = (availableSize.width - innerContentWidth) / CGFloat(component.items.count + 1) innerInset = 0.0 } diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index 8a999cf103..7e19dabd10 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -849,7 +849,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { let albumArtSize = CGSize(width: 48.0, height: 48.0) let makeAlbumArtLayout = self.albumArtNode.asyncLayout() - let applyAlbumArt = makeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: albumArtSize, boundingSize: albumArtSize, intrinsicInsets: UIEdgeInsets())) + let applyAlbumArt = makeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 10.0), imageSize: albumArtSize, boundingSize: albumArtSize, intrinsicInsets: UIEdgeInsets())) applyAlbumArt() let albumArtFrame = CGRect(origin: CGPoint(x: leftInset + sideInset, y: infoVerticalOrigin - 1.0), size: albumArtSize) let previousAlbumArtNodeFrame = self.albumArtNode.frame