Profile main tab

This commit is contained in:
Ilya Laktyushin 2025-08-18 16:53:01 +04:00
parent f828becdb2
commit ea63abbc8a
11 changed files with 460 additions and 92 deletions

2
MODULE.bazel.lock generated
View File

@ -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": {},

View File

@ -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

View File

@ -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

View File

@ -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<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
gift: ProfileGiftsContext.State.StarGift,
gift: StarGift,
valueInfo: StarGift.UniqueGift.ValueInfo,
animateOut: ActionSlot<Action<()>>,
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

View File

@ -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)

View File

@ -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 {

View File

@ -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",

View File

@ -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

View File

@ -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<Empty>()
private let icon = ComponentView<Empty>()
private var iconLayers: [AnyHashable: InlineStickerItemLayer] = [:]
private var component: GiftsTabItemComponent?
func update(component: GiftsTabItemComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, 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<AnyHashable>()
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<EnvironmentType>, 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<Empty>()
private let tabsSeparatorNode: ASDisplayNode
private var didJustReorderTabs = false
private var actionPanel: ComponentView<Empty>?
let isReady = Promise<Bool>()
var didSetIsReady = false
@ -781,51 +959,14 @@ 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 ?? []
@ -1241,6 +1423,11 @@ 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,8 +1510,6 @@ 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)))
@ -1333,48 +1518,136 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
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 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)
}
return PeerInfoPaneSpecifier(key: key, title: title, icons: icons)
}, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition)
for (_, pane) in self.pendingPanes {
let paneTransition: ContainedViewLayoutTransition = .immediate
@ -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)
}
}

View File

@ -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
}

View File

@ -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