Various improvements

This commit is contained in:
Ilya Laktyushin 2023-10-20 21:56:12 +04:00
parent 56db74612e
commit 2107f94bc3
31 changed files with 2512 additions and 202 deletions

View File

@ -994,7 +994,7 @@ public enum PremiumLimitSubject {
case expiringStories case expiringStories
case storiesWeekly case storiesWeekly
case storiesMonthly case storiesMonthly
case storiesChannelBoost(peer: EnginePeer, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, boosted: Bool) case storiesChannelBoost(peer: EnginePeer, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32)
} }
public protocol ComposeController: ViewController { public protocol ComposeController: ViewController {

View File

@ -59,7 +59,10 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
modalStack[modalStack.count - 1].controllers.append(controller) modalStack[modalStack.count - 1].controllers.append(controller)
} }
} else if !modalStack.isEmpty { } else if !modalStack.isEmpty {
controller._presentedInModal = true if modalStack[modalStack.count - 1].isFlat {
} else {
controller._presentedInModal = true
}
if modalStack[modalStack.count - 1].isStandalone { if modalStack[modalStack.count - 1].isStandalone {
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone)) modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone))
} else { } else {

View File

@ -107,6 +107,7 @@ swift_library(
"//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath", "//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
"//submodules/CountrySelectionUI", "//submodules/CountrySelectionUI",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -325,7 +325,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case let .header(_, title, text): case let .header(_, title, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(title + text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(title + text), sectionId: self.section)
case let .createGiveaway(_, title, subtitle, isSelected): case let .createGiveaway(_, title, subtitle, isSelected):
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: .blue, name: "Premium/Giveaway"), title: title, subtitle: subtitle, isSelected: isSelected, sectionId: self.section, action: { return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: .blue, name: "Premium/Giveaway"), title: title, subtitle: subtitle, isSelected: isSelected, sectionId: self.section, action: {
arguments.updateState { state in arguments.updateState { state in
var updatedState = state var updatedState = state
updatedState.mode = .giveaway updatedState.mode = .giveaway
@ -333,7 +333,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} }
}) })
case let .awardUsers(_, title, subtitle, isSelected): case let .awardUsers(_, title, subtitle, isSelected):
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: .violet, name: "Media Editor/Privacy/SelectedUsers"), title: title, subtitle: subtitle, subtitleActive: true, isSelected: isSelected, sectionId: self.section, action: { return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: .violet, name: "Media Editor/Privacy/SelectedUsers"), title: title, subtitle: subtitle, subtitleActive: true, isSelected: isSelected, sectionId: self.section, action: {
var openSelection = false var openSelection = false
arguments.updateState { state in arguments.updateState { state in
var updatedState = state var updatedState = state
@ -361,7 +361,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
default: default:
color = .blue color = .blue
} }
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(prepaidGiveaway.quantity), sectionId: self.section, action: nil) return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(prepaidGiveaway.quantity), sectionId: self.section, action: nil)
case let .subscriptionsHeader(_, text, additionalText): case let .subscriptionsHeader(_, text, additionalText):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section)
case let .subscriptions(_, value): case let .subscriptions(_, value):
@ -1056,7 +1056,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let stateContext = ShareWithPeersScreen.StateContext( let stateContext = ShareWithPeersScreen.StateContext(
context: context, context: context,
subject: .channels(exclude: Set([peerId])), subject: .channels(exclude: Set([peerId]), searchQuery: nil),
initialPeerIds: Set(state.channels.filter { $0 != peerId }) initialPeerIds: Set(state.channels.filter { $0 != peerId })
) )
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in

View File

@ -75,21 +75,6 @@ final class CreateGiveawayFooterItemNode: ItemListControllerFooterItemNode {
private func updateItem() { private func updateItem() {
self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate) self.backgroundNode.updateColor(color: self.item.theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor self.separatorNode.backgroundColor = self.item.theme.rootController.tabBar.separatorColor
// if self.item.isLoading != self.currentIsLoading {
// self.currentIsLoading = self.item.isLoading
//
// if self.currentIsLoading {
// self.buttonNode.transitionToProgress()
// } else {
// self.buttonNode.transitionFromProgress()
// }
// }
// self.buttonNode.pressed = { [weak self] in
// self?.item.action()
// }
} }
override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { override func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit import SwiftSignalKit
import Postbox import Postbox
import TelegramCore import TelegramCore
@ -12,23 +13,16 @@ import AccountContext
import AvatarNode import AvatarNode
public final class GiftOptionItem: ListViewItem, ItemListItem { public final class GiftOptionItem: ListViewItem, ItemListItem {
public struct Icon: Equatable { public enum Icon: Equatable {
public enum Color { public enum Color {
case blue case blue
case green case green
case red case red
case violet case violet
} }
public let color: Color
public let name: String
public init( case peer(EnginePeer)
color: Color, case image(color: Color, name: String)
name: String
) {
self.color = color
self.name = name
}
} }
public enum Font { public enum Font {
@ -60,6 +54,7 @@ public final class GiftOptionItem: ListViewItem, ItemListItem {
let icon: Icon? let icon: Icon?
let title: String let title: String
let titleFont: Font let titleFont: Font
let titleBadge: String?
let subtitle: String? let subtitle: String?
let subtitleFont: SubtitleFont let subtitleFont: SubtitleFont
let subtitleActive: Bool let subtitleActive: Bool
@ -69,12 +64,13 @@ public final class GiftOptionItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let action: (() -> Void)? let action: (() -> Void)?
public init(presentationData: ItemListPresentationData, context: AccountContext, icon: Icon? = nil, title: String, titleFont: Font = .regular, subtitle: String?, subtitleFont: SubtitleFont = .regular, subtitleActive: Bool = false, label: Label? = nil, badge: String? = nil, isSelected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?) { public init(presentationData: ItemListPresentationData, context: AccountContext, icon: Icon? = nil, title: String, titleFont: Font = .regular, titleBadge: String? = nil, subtitle: String?, subtitleFont: SubtitleFont = .regular, subtitleActive: Bool = false, label: Label? = nil, badge: String? = nil, isSelected: Bool? = nil, sectionId: ItemListSectionId, action: (() -> Void)?) {
self.presentationData = presentationData self.presentationData = presentationData
self.icon = icon self.icon = icon
self.context = context self.context = context
self.title = title self.title = title
self.titleFont = titleFont self.titleFont = titleFont
self.titleBadge = titleBadge
self.subtitle = subtitle self.subtitle = subtitle
self.subtitleFont = subtitleFont self.subtitleFont = subtitleFont
self.subtitleActive = subtitleActive self.subtitleActive = subtitleActive
@ -146,7 +142,9 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
} }
fileprivate var iconNode: ASImageNode? fileprivate var iconNode: ASImageNode?
fileprivate var avatarNode: AvatarNode?
private let titleNode: TextNode private let titleNode: TextNode
private let titleBadge = ComponentView<Empty>()
private let statusNode: TextNode private let statusNode: TextNode
private var statusArrowNode: ASImageNode? private var statusArrowNode: ASImageNode?
@ -282,7 +280,10 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
} }
let verticalInset: CGFloat = 10.0 let verticalInset: CGFloat = 10.0
let titleSpacing: CGFloat = 2.0 var titleSpacing: CGFloat = 2.0
if case .bold = item.titleFont {
titleSpacing = 0.0
}
let insets = itemListNeighborsGroupedInsets(neighbors, params) let insets = itemListNeighborsGroupedInsets(neighbors, params)
let separatorHeight = UIScreenPixel let separatorHeight = UIScreenPixel
@ -331,37 +332,72 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
transition = .immediate transition = .immediate
} }
let iconUpdated = currentItem?.icon != item.icon
let iconSize = CGSize(width: 40.0, height: 40.0) let iconSize = CGSize(width: 40.0, height: 40.0)
if let icon = item.icon { if let icon = item.icon {
let iconNode: ASImageNode
if let current = strongSelf.iconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
strongSelf.addSubnode(iconNode)
strongSelf.iconNode = iconNode
}
let colors: [UIColor]
switch icon.color {
case .blue:
colors = [UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x71d4fc)]
case .green:
colors = [UIColor(rgb: 0x54cb68), UIColor(rgb: 0xa0de7e)]
case .red:
colors = [UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)]
case .violet:
colors = [UIColor(rgb: 0xd569ec), UIColor(rgb: 0xe0a2f3)]
}
if iconNode.image == nil {
iconNode.image = generateAvatarImage(size: iconSize, icon: generateTintedImage(image: UIImage(bundleImageName: icon.name), color: .white), iconScale: 1.0, cornerRadius: 20.0, color: .blue, customColors: colors)
}
let iconFrame = CGRect(origin: CGPoint(x: leftInset - 3.0 + editingOffset, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize) let iconFrame = CGRect(origin: CGPoint(x: leftInset - 3.0 + editingOffset, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize)
iconNode.frame = iconFrame
switch icon {
case let .peer(peer):
if let iconNode = strongSelf.iconNode {
strongSelf.iconNode = nil
iconNode.removeFromSupernode()
}
let avatarNode: AvatarNode
if let current = strongSelf.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0)))
strongSelf.addSubnode(avatarNode)
strongSelf.avatarNode = avatarNode
}
avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer)
avatarNode.frame = iconFrame
case let .image(color, name):
if let avatarNode = strongSelf.avatarNode {
strongSelf.avatarNode = nil
avatarNode.removeFromSupernode()
}
let iconNode: ASImageNode
if let current = strongSelf.iconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.displaysAsynchronously = false
strongSelf.addSubnode(iconNode)
strongSelf.iconNode = iconNode
}
let colors: [UIColor]
switch color {
case .blue:
colors = [UIColor(rgb: 0x2a9ef1), UIColor(rgb: 0x71d4fc)]
case .green:
colors = [UIColor(rgb: 0x54cb68), UIColor(rgb: 0xa0de7e)]
case .red:
colors = [UIColor(rgb: 0xff516a), UIColor(rgb: 0xff885e)]
case .violet:
colors = [UIColor(rgb: 0xd569ec), UIColor(rgb: 0xe0a2f3)]
}
if iconNode.image == nil || iconUpdated {
iconNode.image = generateAvatarImage(size: iconSize, icon: generateTintedImage(image: UIImage(bundleImageName: name), color: .white), iconScale: 1.0, cornerRadius: 20.0, color: .blue, customColors: colors)
}
iconNode.frame = iconFrame
}
} else {
if let avatarNode = strongSelf.avatarNode {
strongSelf.avatarNode = nil
avatarNode.removeFromSupernode()
}
if let iconNode = strongSelf.iconNode {
strongSelf.iconNode = nil
iconNode.removeFromSupernode()
}
} }
if let selectableControlSizeAndApply = selectableControlSizeAndApply { if let selectableControlSizeAndApply = selectableControlSizeAndApply {
@ -445,7 +481,8 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
} else { } else {
titleVerticalOriginY = floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) titleVerticalOriginY = floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)
} }
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset, y: titleVerticalOriginY), size: titleLayout.size)) let titleFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset + avatarInset, y: titleVerticalOriginY), size: titleLayout.size)
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
var badgeOffset: CGFloat = 0.0 var badgeOffset: CGFloat = 0.0
if badgeLayout.size.width > 0.0 { if badgeLayout.size.width > 0.0 {
@ -528,6 +565,29 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: strongSelf.backgroundNode.frame.height + UIScreenPixel + UIScreenPixel))
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
if let badge = item.titleBadge {
let badgeSize = strongSelf.titleBadge.update(
transition: .immediate,
component: AnyComponent(
BoostIconComponent(text: badge)
),
environment: {},
containerSize: CGSize(width: params.width, height: 100.0)
)
if let view = strongSelf.titleBadge.view {
if view.superview == nil {
strongSelf.view.addSubview(view)
}
let badgeFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floorToScreenPixels(titleFrame.midY - badgeSize.height / 2.0) - 1.0), size: badgeSize)
view.frame = badgeFrame
}
} else {
if let view = strongSelf.titleBadge.view {
view.removeFromSuperview()
}
}
} }
}) })
} }

View File

@ -130,6 +130,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let theme = environment.theme let theme = environment.theme
let strings = environment.strings let strings = environment.strings
let dateTimeFormat = environment.dateTimeFormat let dateTimeFormat = environment.dateTimeFormat
let accountContext = context.component.context
let state = context.state let state = context.state
let giftCode = component.giftCode let giftCode = component.giftCode
@ -239,7 +240,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
Button( Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)), content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)),
action: { action: {
if let peer = fromPeer { if let peer = fromPeer, peer.id != accountContext.account.peerId {
component.openPeer(peer) component.openPeer(peer)
Queue.mainQueue().after(1.0, { Queue.mainQueue().after(1.0, {
component.cancel(false) component.cancel(false)
@ -258,7 +259,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
Button( Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)), content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)),
action: { action: {
if let peer = toPeer { if let peer = toPeer, peer.id != accountContext.account.peerId {
component.openPeer(peer) component.openPeer(peer)
Queue.mainQueue().after(1.0, { Queue.mainQueue().after(1.0, {
component.cancel(false) component.cancel(false)
@ -309,7 +310,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
component.openMessage(messageId) component.openMessage(messageId)
} }
Queue.mainQueue().after(1.0) { Queue.mainQueue().after(1.0) {
component.cancel(true) component.cancel(false)
} }
} }
) )
@ -479,11 +480,15 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
giftCode: context.component.giftCode, giftCode: context.component.giftCode,
action: context.component.action, action: context.component.action,
cancel: { animate in cancel: { animate in
animateOut.invoke(Action { _ in if animate {
if let controller = controller() { animateOut.invoke(Action { _ in
controller.dismiss(completion: nil) if let controller = controller() {
} controller.dismiss(completion: nil)
}) }
})
} else if let controller = controller() {
controller.dismiss(animated: false, completion: nil)
}
}, },
openPeer: context.component.openPeer, openPeer: context.component.openPeer,
openMessage: context.component.openMessage, openMessage: context.component.openMessage,

View File

@ -723,7 +723,7 @@ private final class LimitSheetContent: CombinedComponent {
var premiumLimits: EngineConfiguration.UserLimits var premiumLimits: EngineConfiguration.UserLimits
var isPremium = false var isPremium = false
var boosted = false var myBoostCount: Int32 = 0
var cachedCloseImage: (UIImage, PresentationTheme)? var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)?
@ -1045,7 +1045,7 @@ private final class LimitSheetContent: CombinedComponent {
string = strings.Premium_MaxStoriesMonthlyNoPremiumText("\(limit)").string string = strings.Premium_MaxStoriesMonthlyNoPremiumText("\(limit)").string
} }
buttonAnimationName = nil buttonAnimationName = nil
case let .storiesChannelBoost(peer, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, boosted): case let .storiesChannelBoost(peer, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, myBoostCount):
if link == nil, !isCurrent, state.initialized { if link == nil, !isCurrent, state.initialized {
peerShortcutChild = peerShortcut.update( peerShortcutChild = peerShortcut.update(
component: Button( component: Button(
@ -1054,7 +1054,7 @@ private final class LimitSheetContent: CombinedComponent {
context: component.context, context: component.context,
theme: environment.theme, theme: environment.theme,
peer: peer, peer: peer,
badge: "X2" badge: myBoostCount > 0 ? "\(myBoostCount)" : nil
) )
), ),
action: { action: {
@ -1098,11 +1098,11 @@ private final class LimitSheetContent: CombinedComponent {
) )
} }
if boosted && state.boosted != boosted { if myBoostCount > 0 && state.myBoostCount != myBoostCount {
state.boosted = boosted state.myBoostCount = myBoostCount
boostUpdated = true boostUpdated = true
} }
useAlternateText = boosted useAlternateText = myBoostCount > 0
iconName = "Premium/Boost" iconName = "Premium/Boost"
badgeText = "\(component.count)" badgeText = "\(component.count)"
@ -1157,7 +1157,7 @@ private final class LimitSheetContent: CombinedComponent {
premiumTitle = "" premiumTitle = ""
if boosted { if myBoostCount > 0 {
let prefixString = isCurrent ? strings.ChannelBoost_YouBoostedChannelText(peer.compactDisplayTitle).string : strings.ChannelBoost_YouBoostedOtherChannelText let prefixString = isCurrent ? strings.ChannelBoost_YouBoostedChannelText(peer.compactDisplayTitle).string : strings.ChannelBoost_YouBoostedOtherChannelText
let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1) let storiesString = strings.ChannelBoost_StoriesPerDay(level + 1)
@ -1205,6 +1205,10 @@ private final class LimitSheetContent: CombinedComponent {
if case .folders = subject, !state.isPremium { if case .folders = subject, !state.isPremium {
reachedMaximumLimit = false reachedMaximumLimit = false
} }
if case .storiesChannelBoost = component.subject {
reachedMaximumLimit = false
}
let contentSize: CGSize let contentSize: CGSize
if state.initialized { if state.initialized {
@ -1648,7 +1652,7 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
case storiesWeekly case storiesWeekly
case storiesMonthly case storiesMonthly
case storiesChannelBoost(peer: EnginePeer, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, boosted: Bool) case storiesChannelBoost(peer: EnginePeer, isCurrent: Bool, level: Int32, currentLevelBoosts: Int32, nextLevelBoosts: Int32?, link: String?, myBoostCount: Int32)
} }
private let context: AccountContext private let context: AccountContext
@ -1742,8 +1746,7 @@ private final class PeerShortcutComponent: Component {
private let avatarNode: AvatarNode private let avatarNode: AvatarNode
private let text = ComponentView<Empty>() private let text = ComponentView<Empty>()
private let badgeBackground = UIView() private let badge = ComponentView<Empty>()
private let badgeText = ComponentView<Empty>()
private var component: PeerShortcutComponent? private var component: PeerShortcutComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -1756,12 +1759,8 @@ private final class PeerShortcutComponent: Component {
self.backgroundView.clipsToBounds = true self.backgroundView.clipsToBounds = true
self.backgroundView.layer.cornerRadius = 16.0 self.backgroundView.layer.cornerRadius = 16.0
self.badgeBackground.clipsToBounds = true
self.badgeBackground.backgroundColor = UIColor(rgb: 0x9671ff)
self.addSubview(self.backgroundView) self.addSubview(self.backgroundView)
self.addSubnode(self.avatarNode) self.addSubnode(self.avatarNode)
self.addSubview(self.badgeBackground)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -1803,35 +1802,26 @@ private final class PeerShortcutComponent: Component {
} }
if let badge = component.badge { if let badge = component.badge {
let badgeSize = self.badgeText.update( let badgeSize = self.badge.update(
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent( BoostIconComponent(text: badge)
text: .plain(NSAttributedString(string: badge, font: Font.with(size: 11.0, design: .round, weight: .bold), textColor: .white, paragraphAlignment: .left))
)
), ),
environment: {}, environment: {},
containerSize: availableSize containerSize: availableSize
) )
if let view = self.badgeText.view { if let view = self.badge.view {
if view.superview == nil { if view.superview == nil {
self.addSubview(view) self.addSubview(view)
} }
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width - badgeSize.width / 2.0), y: -2.0), size: badgeSize) let badgeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width - badgeSize.width / 2.0), y: -5.0), size: badgeSize)
view.frame = textFrame view.frame = badgeFrame
let backgroundFrame = textFrame.insetBy(dx: -5.0, dy: -3.0)
self.badgeBackground.frame = backgroundFrame
self.badgeBackground.layer.cornerRadius = backgroundFrame.height / 2.0
self.badgeBackground.isHidden = false
} }
} else { } else {
if let view = self.badgeText.view { if let view = self.badge.view {
view.removeFromSuperview() view.removeFromSuperview()
} }
self.badgeBackground.isHidden = true
} }
self.backgroundView.frame = CGRect(origin: .zero, size: size) self.backgroundView.frame = CGRect(origin: .zero, size: size)
@ -1848,3 +1838,94 @@ private final class PeerShortcutComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
} }
} }
public final class BoostIconComponent: Component {
let text: String
public init(text: String) {
self.text = text
}
public static func ==(lhs: BoostIconComponent, rhs: BoostIconComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
return true
}
public final class View: UIView {
private let badgeBackground = UIView()
private let badgeIcon: UIImageView
private let badgeText = ComponentView<Empty>()
private var component: BoostIconComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.badgeIcon = UIImageView(image: UIImage(bundleImageName: "Premium/BoostButtonIcon"))
super.init(frame: frame)
self.badgeBackground.clipsToBounds = true
self.badgeBackground.backgroundColor = UIColor(rgb: 0x9671ff)
self.addSubview(self.badgeBackground)
self.addSubview(self.badgeIcon)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BoostIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let textSize = self.badgeText.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.text, font: Font.with(size: 10.0, design: .round, weight: .bold), textColor: .white, paragraphAlignment: .left))
)
),
environment: {},
containerSize: availableSize
)
let spacing: CGFloat = 2.0
var totalWidth = textSize.width + spacing
var iconSize = CGSize()
if let icon = self.badgeIcon.image {
iconSize = CGSize(width: icon.size.width * 0.9, height: icon.size.height * 0.9)
totalWidth += icon.size.width
}
let size = CGSize(width: totalWidth + 8.0, height: 19.0)
let iconFrame = CGRect(x: floorToScreenPixels((size.width - totalWidth) / 2.0 + 1.0), y: 4.0 + UIScreenPixel, width: iconSize.width, height: iconSize.height)
self.badgeIcon.frame = iconFrame
let textFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: 4.0), size: textSize)
if let view = self.badgeText.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = textFrame
}
self.badgeBackground.frame = CGRect(origin: .zero, size: size)
self.badgeBackground.layer.cornerRadius = size.height / 2.0
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,267 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
final class BoostsTabsItem: ListViewItem, ItemListItem {
enum Tab {
case boosts
case gifts
}
let theme: PresentationTheme
let boostsText: String
let giftsText: String
let selectedTab: Tab
let sectionId: ItemListSectionId
let selectionUpdated: (Tab) -> Void
init(theme: PresentationTheme, boostsText: String, giftsText: String, selectedTab: Tab, sectionId: ItemListSectionId, selectionUpdated: @escaping (Tab) -> Void) {
self.theme = theme
self.boostsText = boostsText
self.giftsText = giftsText
self.selectedTab = selectedTab
self.sectionId = sectionId
self.selectionUpdated = selectionUpdated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = BoostsTabsItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? BoostsTabsItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class BoostsTabsItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let boostsButton: HighlightTrackingButtonNode
private let boostsTextNode: TextNode
private let giftsButton: HighlightTrackingButtonNode
private let giftsTextNode: TextNode
private let selectionNode: ASImageNode
private var item: BoostsTabsItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.boostsButton = HighlightTrackingButtonNode()
self.boostsTextNode = TextNode()
self.boostsTextNode.isUserInteractionEnabled = false
self.boostsTextNode.displaysAsynchronously = false
self.giftsButton = HighlightTrackingButtonNode()
self.giftsTextNode = TextNode()
self.giftsTextNode.isUserInteractionEnabled = false
self.giftsTextNode.displaysAsynchronously = false
self.selectionNode = ASImageNode()
self.selectionNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.boostsTextNode)
self.addSubnode(self.giftsTextNode)
self.addSubnode(self.selectionNode)
self.addSubnode(self.boostsButton)
self.addSubnode(self.giftsButton)
self.boostsButton.addTarget(self, action: #selector(self.boostsPressed), forControlEvents: .touchUpInside)
self.giftsButton.addTarget(self, action: #selector(self.giftsPressed), forControlEvents: .touchUpInside)
}
@objc private func boostsPressed() {
if let item = self.item {
item.selectionUpdated(.boosts)
}
}
@objc private func giftsPressed() {
if let item = self.item {
item.selectionUpdated(.gifts)
}
}
func asyncLayout() -> (_ item: BoostsTabsItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeBoostsTextLayout = TextNode.asyncLayout(self.boostsTextNode)
let makeGiftsTextLayout = TextNode.asyncLayout(self.giftsTextNode)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let accentColor = item.theme.list.itemAccentColor
let secondaryColor = item.theme.list.itemSecondaryTextColor
let (boostsTextLayout, boostsTextApply) = makeBoostsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.boostsText, font: Font.medium(14.0), textColor: item.selectedTab == .boosts ? accentColor : secondaryColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (giftsTextLayout, giftsTextApply) = makeGiftsTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.giftsText, font: Font.medium(14.0), textColor: item.selectedTab == .gifts ? accentColor : secondaryColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
contentSize = CGSize(width: params.width, height: 48.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if themeUpdated {
strongSelf.selectionNode.image = generateImage(CGSize(width: 4.0, height: 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(item.theme.list.itemAccentColor.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: size.width, height: 4.0)), cornerRadius: 2.0)
context.addPath(path.cgPath)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 2, topCapHeight: 0)
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let _ = boostsTextApply()
let _ = giftsTextApply()
strongSelf.boostsTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 16.0), size: boostsTextLayout.size)
strongSelf.giftsTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0 + boostsTextLayout.size.width + 27.0, y: 16.0), size: giftsTextLayout.size)
strongSelf.boostsButton.frame = strongSelf.boostsTextNode.frame.insetBy(dx: -10.0, dy: -10.0)
strongSelf.giftsButton.frame = strongSelf.giftsTextNode.frame.insetBy(dx: -10.0, dy: -10.0)
let selectionHeight: CGFloat = 3.0
let selectionFrame: CGRect
switch item.selectedTab {
case .boosts:
selectionFrame = CGRect(x: strongSelf.boostsTextNode.frame.minX, y: layoutSize.height - selectionHeight, width: strongSelf.boostsTextNode.frame.width, height: selectionHeight)
case .gifts:
selectionFrame = CGRect(x: strongSelf.giftsTextNode.frame.minX, y: layoutSize.height - selectionHeight, width: strongSelf.giftsTextNode.frame.width, height: selectionHeight)
}
var transition: ContainedViewLayoutTransition = .immediate
if let currentItem, currentItem.selectedTab != item.selectedTab {
transition = .animated(duration: 0.3, curve: .spring)
}
transition.updateFrame(node: strongSelf.selectionNode, frame: selectionFrame)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -35,8 +35,9 @@ private final class ChannelStatsControllerArguments {
let expandBoosters: () -> Void let expandBoosters: () -> Void
let openGifts: () -> Void let openGifts: () -> Void
let createPrepaidGiveaway: (PrepaidGiveaway) -> Void let createPrepaidGiveaway: (PrepaidGiveaway) -> Void
let updateGiftsSelected: (Bool) -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void) { init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) {
self.context = context self.context = context
self.loadDetailedGraph = loadDetailedGraph self.loadDetailedGraph = loadDetailedGraph
self.openMessageStats = openMessage self.openMessageStats = openMessage
@ -47,6 +48,7 @@ private final class ChannelStatsControllerArguments {
self.expandBoosters = expandBoosters self.expandBoosters = expandBoosters
self.openGifts = openGifts self.openGifts = openGifts
self.createPrepaidGiveaway = createPrepaidGiveaway self.createPrepaidGiveaway = createPrepaidGiveaway
self.updateGiftsSelected = updateGiftsSelected
} }
} }
@ -115,7 +117,8 @@ private enum StatsEntry: ItemListNodeEntry {
case boostersTitle(PresentationTheme, String) case boostersTitle(PresentationTheme, String)
case boostersPlaceholder(PresentationTheme, String) case boostersPlaceholder(PresentationTheme, String)
case booster(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer?, Int32) case boosterTabs(PresentationTheme, String, String, Bool)
case booster(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer?, Int32, ChannelBoostersContext.State.Boost.Flags, Int32)
case boostersExpand(PresentationTheme, String) case boostersExpand(PresentationTheme, String)
case boostersInfo(PresentationTheme, String) case boostersInfo(PresentationTheme, String)
@ -156,7 +159,7 @@ private enum StatsEntry: ItemListNodeEntry {
return StatsSection.boostOverview.rawValue return StatsSection.boostOverview.rawValue
case .boostPrepaidTitle, .boostPrepaid, .boostPrepaidInfo: case .boostPrepaidTitle, .boostPrepaid, .boostPrepaidInfo:
return StatsSection.boostPrepaid.rawValue return StatsSection.boostPrepaid.rawValue
case .boostersTitle, .boostersPlaceholder, .booster, .boostersExpand, .boostersInfo: case .boostersTitle, .boostersPlaceholder, .boosterTabs, .booster, .boostersExpand, .boostersInfo:
return StatsSection.boosters.rawValue return StatsSection.boosters.rawValue
case .boostLinkTitle, .boostLink, .boostLinkInfo: case .boostLinkTitle, .boostLink, .boostLinkInfo:
return StatsSection.boostLink.rawValue return StatsSection.boostLink.rawValue
@ -227,8 +230,10 @@ private enum StatsEntry: ItemListNodeEntry {
return 2101 return 2101
case .boostersPlaceholder: case .boostersPlaceholder:
return 2102 return 2102
case let .booster(index, _, _, _, _): case .boosterTabs:
return 2103 + index return 2103
case let .booster(index, _, _, _, _, _, _):
return 2104 + index
case .boostersExpand: case .boostersExpand:
return 10000 return 10000
case .boostersInfo: case .boostersInfo:
@ -428,8 +433,14 @@ private enum StatsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsExpires): case let .boosterTabs(lhsTheme, lhsBoostText, lhsGiftText, lhsGiftSelected):
if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsExpires) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsExpires == rhsExpires { if case let .boosterTabs(rhsTheme, rhsBoostText, rhsGiftText, rhsGiftSelected) = rhs, lhsTheme === rhsTheme, lhsBoostText == rhsBoostText, lhsGiftText == rhsGiftText, lhsGiftSelected == rhsGiftSelected {
return true
} else {
return false
}
case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsCount, lhsFlags, lhsExpires):
if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsCount, rhsFlags, rhsExpires) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsCount == rhsCount, lhsFlags == rhsFlags, lhsExpires == rhsExpires {
return true return true
} else { } else {
return false return false
@ -533,17 +544,34 @@ private enum StatsEntry: ItemListNodeEntry {
}, contextAction: { node, gesture in }, contextAction: { node, gesture in
arguments.contextAction(message.id, node, gesture) arguments.contextAction(message.id, node, gesture)
}) })
case let .booster(_, _, dateTimeFormat, peer, expires): case let .boosterTabs(_, boostText, giftText, giftSelected):
let _ = dateTimeFormat return BoostsTabsItem(theme: presentationData.theme, boostsText: boostText, giftsText: giftText, selectedTab: giftSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in
let _ = peer arguments.updateGiftsSelected(tab == .gifts)
let _ = expires })
return ItemListTextItem(presentationData: presentationData, text: .markdown("text"), sectionId: self.section) case let .booster(_, _, _, peer, count, flags, expires):
// let expiresValue = stringForMediumDate(timestamp: expires, strings: presentationData.strings, dateTimeFormat: dateTimeFormat) let expiresValue = stringForDate(timestamp: expires, strings: presentationData.strings)
// return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer?.id != nil && peer?.id != arguments.context.account.peerId, sectionId: self.section, action: { let expiresString = presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string
// if let peer {
// arguments.openPeer(peer) let title: String
// } let icon: GiftOptionItem.Icon
// }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) if let peer {
title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
icon = .peer(peer)
} else {
if flags.contains(.isUnclaimed) {
title = "Unclaimed"
icon = .image(color: .red, name: "Premium/Unclaimed")
} else if flags.contains(.isGiveaway) {
title = "To be distributed"
icon = .image(color: .blue, name: "Premium/ToBeDistributed")
} else {
title = "Unknown"
icon = .image(color: .red, name: "Premium/ToBeDistributed")
}
}
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: icon, title: title, titleFont: .bold, titleBadge: count > 1 ? "\(count)" : nil, subtitle: expiresString, sectionId: self.section, action: peer != nil && peer?.id != arguments.context.account.peerId ? {
arguments.openPeer(peer!)
} : nil)
case let .boostersExpand(theme, title): case let .boostersExpand(theme, title):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
arguments.expandBoosters() arguments.expandBoosters()
@ -579,7 +607,7 @@ private enum StatsEntry: ItemListNodeEntry {
default: default:
color = .blue color = .blue
} }
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: GiftOptionItem.Icon(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(prepaidGiveaway.quantity), sectionId: self.section, action: { return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity)", subtitle: subtitle, label: nil, sectionId: self.section, action: {
arguments.createPrepaidGiveaway(prepaidGiveaway) arguments.createPrepaidGiveaway(prepaidGiveaway)
}) })
} }
@ -594,15 +622,18 @@ public enum ChannelStatsSection {
private struct ChannelStatsControllerState: Equatable { private struct ChannelStatsControllerState: Equatable {
let section: ChannelStatsSection let section: ChannelStatsSection
let boostersExpanded: Bool let boostersExpanded: Bool
let giftsSelected: Bool
init() { init() {
self.section = .stats self.section = .stats
self.boostersExpanded = false self.boostersExpanded = false
self.giftsSelected = false
} }
init(section: ChannelStatsSection, boostersExpanded: Bool) { init(section: ChannelStatsSection, boostersExpanded: Bool, giftsSelected: Bool) {
self.section = section self.section = section
self.boostersExpanded = boostersExpanded self.boostersExpanded = boostersExpanded
self.giftsSelected = giftsSelected
} }
static func ==(lhs: ChannelStatsControllerState, rhs: ChannelStatsControllerState) -> Bool { static func ==(lhs: ChannelStatsControllerState, rhs: ChannelStatsControllerState) -> Bool {
@ -612,20 +643,27 @@ private struct ChannelStatsControllerState: Equatable {
if lhs.boostersExpanded != rhs.boostersExpanded { if lhs.boostersExpanded != rhs.boostersExpanded {
return false return false
} }
if lhs.giftsSelected != rhs.giftsSelected {
return false
}
return true return true
} }
func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState { func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded) return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, giftsSelected: self.giftsSelected)
} }
func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState { func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded) return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, giftsSelected: self.giftsSelected)
}
func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState {
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, giftsSelected: giftsSelected)
} }
} }
private func channelStatsControllerEntries(state: ChannelStatsControllerState, peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, interactions: [MessageId: ChannelStatsMessageInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, presentationData: PresentationData) -> [StatsEntry] { private func channelStatsControllerEntries(state: ChannelStatsControllerState, peer: EnginePeer?, data: ChannelStats?, messages: [Message]?, interactions: [MessageId: ChannelStatsMessageInteractions]?, boostData: ChannelBoostStatus?, boostersState: ChannelBoostersContext.State?, giftsState: ChannelBoostersContext.State?, presentationData: PresentationData) -> [StatsEntry] {
var entries: [StatsEntry] = [] var entries: [StatsEntry] = []
switch state.section { switch state.section {
@ -735,6 +773,19 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
entries.append(.boostersPlaceholder(presentationData.theme, boostersPlaceholder)) entries.append(.boostersPlaceholder(presentationData.theme, boostersPlaceholder))
} }
var boostsCount: Int32 = 0
if let boostersState {
boostsCount = boostersState.count
}
var giftsCount: Int32 = 0
if let giftsState {
giftsCount = giftsState.count
}
if boostsCount > 0 && giftsCount > 0 && boostsCount != giftsCount {
entries.append(.boosterTabs(presentationData.theme, "\(boostsCount) Boosts", "\(giftsCount) Gifts", state.giftsSelected))
}
if let boostersState { if let boostersState {
var boosterIndex: Int32 = 0 var boosterIndex: Int32 = 0
@ -747,7 +798,7 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
} }
for booster in boosters { for booster in boosters {
entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster.peer, booster.expires)) entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster.peer, booster.multiplier, booster.flags, booster.expires))
boosterIndex += 1 boosterIndex += 1
} }
@ -773,8 +824,8 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
} }
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, statsDatacenterId: Int32?) -> ViewController { public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, statsDatacenterId: Int32?) -> ViewController {
let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false), ignoreRepeated: true) let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false)) let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false))
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) }) statePromise.set(stateValue.modify { f($0) })
} }
@ -813,7 +864,8 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
} else { } else {
boostData = .single(nil) |> then(context.engine.peers.getChannelBoostStatus(peerId: peerId)) boostData = .single(nil) |> then(context.engine.peers.getChannelBoostStatus(peerId: peerId))
} }
let boostersContext = ChannelBoostersContext(account: context.account, peerId: peerId) let boostsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false)
let giftsContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: true)
var presentImpl: ((ViewController) -> Void)? var presentImpl: ((ViewController) -> Void)?
var pushImpl: ((ViewController) -> Void)? var pushImpl: ((ViewController) -> Void)?
@ -885,6 +937,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
createPrepaidGiveaway: { prepaidGiveaway in createPrepaidGiveaway: { prepaidGiveaway in
let controller = createGiveawayController(context: context, peerId: peerId, subject: .prepaid(prepaidGiveaway)) let controller = createGiveawayController(context: context, peerId: peerId, subject: .prepaid(prepaidGiveaway))
pushImpl?(controller) pushImpl?(controller)
},
updateGiftsSelected: { selected in
updateState { $0.withUpdatedGiftsSelected(selected) }
}) })
let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil) let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil)
@ -905,11 +960,12 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
dataPromise.get(), dataPromise.get(),
messagesPromise.get(), messagesPromise.get(),
boostData, boostData,
boostersContext.state, boostsContext.state,
giftsContext.state,
longLoadingSignal longLoadingSignal
) )
|> deliverOnMainQueue |> deliverOnMainQueue
|> map { presentationData, state, peer, data, messageView, boostData, boostersState, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in |> map { presentationData, state, peer, data, messageView, boostData, boostersState, giftsState, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previous = previousData.swap(data) let previous = previousData.swap(data)
var emptyStateItem: ItemListControllerEmptyStateItem? var emptyStateItem: ItemListControllerEmptyStateItem?
switch state.section { switch state.section {
@ -939,7 +995,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.Stats_Statistics, presentationData.strings.Stats_Boosts], state.section == .boosts ? 1 : 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.Stats_Statistics, presentationData.strings.Stats_Boosts], state.section == .boosts ? 1 : 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(state: state, peer: peer, data: data, messages: messages, interactions: interactions, boostData: boostData, boostersState: boostersState, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelStatsControllerEntries(state: state, peer: peer, data: data, messages: messages, interactions: interactions, boostData: boostData, boostersState: boostersState, giftsState: giftsState, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -959,7 +1015,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
controller.visibleBottomContentOffsetChanged = { offset in controller.visibleBottomContentOffsetChanged = { offset in
let state = stateValue.with { $0 } let state = stateValue.with { $0 }
if case let .known(value) = offset, value < 100.0, case .boosts = state.section, state.boostersExpanded { if case let .known(value) = offset, value < 100.0, case .boosts = state.section, state.boostersExpanded {
boostersContext.loadMore() boostsContext.loadMore()
} }
} }
controller.titleControlValueChanged = { value in controller.titleControlValueChanged = { value in

View File

@ -97,7 +97,7 @@ func _internal_updatePeerNameColorAndEmoji(account: Account, peerId: EnginePeer.
let accountPeerId = account.peerId let accountPeerId = account.peerId
return account.postbox.transaction { transaction -> Signal<Peer, NoError> in return account.postbox.transaction { transaction -> Signal<Peer, NoError> in
guard let peer = transaction.getPeer(account.peerId) as? TelegramChannel else { guard let peer = transaction.getPeer(peerId) as? TelegramChannel else {
return .complete() return .complete()
} }
updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId)], update: { _, updated in updatePeersCustom(transaction: transaction, peers: [peer.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId)], update: { _, updated in

View File

@ -47,7 +47,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
private var giveaway: TelegramMediaGiveaway? private var giveaway: TelegramMediaGiveaway?
private let buttonNode: ChatMessageAttachedContentButtonNode private let buttonNode: ChatMessageAttachedContentButtonNode
private let channelButton: PeerButtonNode private let channelButtons: PeerButtonsStackNode
override public var visibility: ListViewItemNodeVisibility { override public var visibility: ListViewItemNodeVisibility {
didSet { didSet {
@ -95,7 +95,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
self.badgeTextNode = TextNode() self.badgeTextNode = TextNode()
self.buttonNode = ChatMessageAttachedContentButtonNode() self.buttonNode = ChatMessageAttachedContentButtonNode()
self.channelButton = PeerButtonNode() self.channelButtons = PeerButtonsStackNode()
super.init() super.init()
@ -107,7 +107,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
self.addSubnode(self.dateTitleNode) self.addSubnode(self.dateTitleNode)
self.addSubnode(self.dateTextNode) self.addSubnode(self.dateTextNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
self.addSubnode(self.channelButton) self.addSubnode(self.channelButtons)
self.addSubnode(self.animationNode) self.addSubnode(self.animationNode)
self.addSubnode(self.badgeBackgroundNode) self.addSubnode(self.badgeBackgroundNode)
self.addSubnode(self.badgeTextNode) self.addSubnode(self.badgeTextNode)
@ -129,6 +129,13 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceView, gesture, value) item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceView, gesture, value)
} }
self.channelButtons.openPeer = { [weak self] peer in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
}
} }
override public func accessibilityActivate() -> Bool { override public func accessibilityActivate() -> Bool {
@ -185,7 +192,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode) let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode)
let makeChannelLayout = PeerButtonNode.asyncLayout(self.channelButton) let makeChannelsLayout = PeerButtonsStackNode.asyncLayout(self.channelButtons)
let currentItem = self.item let currentItem = self.item
@ -207,12 +214,13 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
incoming = false incoming = false
} }
let backgroundColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first! : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill.first!
let textColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor let textColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
let accentColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor : item.presentationData.theme.theme.chat.message.outgoing.accentTextColor let accentColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor : item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
var updatedBadgeImage: UIImage? var updatedBadgeImage: UIImage?
if themeUpdated { if themeUpdated {
updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil) updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: backgroundColor, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)
} }
let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: .white) let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: .white)
@ -413,13 +421,18 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
maxContentWidth = max(maxContentWidth, dateTitleLayout.size.width) maxContentWidth = max(maxContentWidth, dateTitleLayout.size.width)
maxContentWidth = max(maxContentWidth, dateTextLayout.size.width) maxContentWidth = max(maxContentWidth, dateTextLayout.size.width)
maxContentWidth = max(maxContentWidth, buttonWidth) maxContentWidth = max(maxContentWidth, buttonWidth)
maxContentWidth += 30.0
var channelPeers: [EnginePeer] = []
var channelPeer: EnginePeer? if let channelPeerIds = giveaway?.channelPeerIds {
if let channelPeerId = giveaway?.channelPeerIds.first, let peer = item.message.peers[channelPeerId] { for peerId in channelPeerIds {
channelPeer = EnginePeer(peer) if let peer = item.message.peers[peerId] {
channelPeers.append(EnginePeer(peer))
}
}
} }
let (_, continueChannelLayout) = makeChannelLayout(item.context, maxContentWidth - 30.0, channelPeer, accentColor, accentColor.withAlphaComponent(0.1)) let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 250.0, channelPeers, accentColor, accentColor.withAlphaComponent(0.1))
maxContentWidth = max(maxContentWidth, channelsWidth)
maxContentWidth += 30.0
let contentWidth = maxContentWidth + layoutConstants.text.bubbleInsets.right * 2.0 let contentWidth = maxContentWidth + layoutConstants.text.bubbleInsets.right * 2.0
@ -427,7 +440,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0, 33.0) let (buttonSize, buttonApply) = continueLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0, 33.0)
let buttonSpacing: CGFloat = 4.0 let buttonSpacing: CGFloat = 4.0
let (channelButtonSize, channelButtonApply) = continueChannelLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0) let (channelButtonsSize, channelButtonsApply) = continueChannelLayout(boundingWidth - layoutConstants.text.bubbleInsets.right * 2.0)
let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets) let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets)
@ -436,7 +449,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
if countriesTextLayout.size.height > 0.0 { if countriesTextLayout.size.height > 0.0 {
layoutSize.height += countriesTextLayout.size.height + 7.0 layoutSize.height += countriesTextLayout.size.height + 7.0
} }
layoutSize.height += channelButtonSize.height layoutSize.height += channelButtonsSize.height
if let statusSizeAndApply = statusSizeAndApply { if let statusSizeAndApply = statusSizeAndApply {
layoutSize.height += statusSizeAndApply.0.height - 4.0 layoutSize.height += statusSizeAndApply.0.height - 4.0
@ -462,7 +475,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
let _ = countriesTextApply() let _ = countriesTextApply()
let _ = dateTitleApply() let _ = dateTitleApply()
let _ = dateTextApply() let _ = dateTextApply()
let _ = channelButtonApply() let _ = channelButtonsApply()
let _ = buttonApply(animation) let _ = buttonApply(animation)
let smallSpacing: CGFloat = 2.0 let smallSpacing: CGFloat = 2.0
@ -493,8 +506,8 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
strongSelf.participantsTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTextLayout.size.width) / 2.0), y: originY), size: participantsTextLayout.size) strongSelf.participantsTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - participantsTextLayout.size.width) / 2.0), y: originY), size: participantsTextLayout.size)
originY += participantsTextLayout.size.height + smallSpacing * 2.0 + 3.0 originY += participantsTextLayout.size.height + smallSpacing * 2.0 + 3.0
strongSelf.channelButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - channelButtonSize.width) / 2.0), y: originY), size: channelButtonSize) strongSelf.channelButtons.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - channelButtonsSize.width) / 2.0), y: originY), size: channelButtonsSize)
originY += channelButtonSize.height originY += channelButtonsSize.height
if countriesTextLayout.size.height > 0.0 { if countriesTextLayout.size.height > 0.0 {
originY += smallSpacing * 2.0 + 3.0 originY += smallSpacing * 2.0 + 3.0
@ -566,6 +579,9 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
} }
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.channelButtons.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
if self.buttonNode.frame.contains(point) { if self.buttonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore) return ChatMessageBubbleContentTapAction(content: .ignore)
} }
@ -578,10 +594,6 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
@objc private func buttonPressed() { @objc private func buttonPressed() {
if let item = self.item { if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, .default) let _ = item.controllerInteraction.openMessage(item.message, .default)
self.buttonNode.startShimmering()
Queue.mainQueue().after(0.75) {
self.buttonNode.stopShimmering()
}
} }
} }
@ -600,6 +612,126 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
} }
} }
private final class PeerButtonsStackNode: ASDisplayNode {
var buttonNodes: [PeerButtonNode] = []
var openPeer: (EnginePeer) -> Void = { _ in }
static func asyncLayout(_ current: PeerButtonsStackNode) -> (_ context: AccountContext, _ width: CGFloat, _ peers: [EnginePeer], _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> PeerButtonsStackNode)) {
let currentChannelButtons = current.buttonNodes.isEmpty ? nil : current.buttonNodes
let maybeMakeChannelButtons = current.buttonNodes.map(PeerButtonNode.asyncLayout)
return { context, width, peers, titleColor, backgroundColor in
let targetNode = current
var buttonNodes: [PeerButtonNode] = []
let makeChannelButtonLayouts: [(_ context: AccountContext, _ width: CGFloat, _ peer: EnginePeer?, _ titleColor: UIColor, _ backgroundColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> PeerButtonNode))]
if let currentChannelButtons {
buttonNodes = currentChannelButtons
makeChannelButtonLayouts = maybeMakeChannelButtons
} else {
for _ in peers {
buttonNodes.append(PeerButtonNode())
}
makeChannelButtonLayouts = buttonNodes.map(PeerButtonNode.asyncLayout)
}
var maxWidth = 0.0
let buttonHeight: CGFloat = 24.0
let horizontalButtonSpacing: CGFloat = 4.0
let verticalButtonSpacing: CGFloat = 6.0
var sizes: [CGSize] = []
var groups: [[Int]] = []
var currentGroup: [Int] = []
var buttonContinues: [(CGFloat) -> (CGSize, () -> PeerButtonNode)] = []
for i in 0 ..< makeChannelButtonLayouts.count {
let peer = peers[i]
let makeChannelButtonLayout = makeChannelButtonLayouts[i]
let (buttonWidth, buttonContinue) = makeChannelButtonLayout(context, width, peer, titleColor, backgroundColor)
sizes.append(CGSize(width: buttonWidth, height: buttonHeight))
buttonContinues.append(buttonContinue)
var itemsWidth: CGFloat = 0.0
for j in currentGroup {
itemsWidth += sizes[j].width
}
itemsWidth += buttonWidth
itemsWidth += CGFloat(currentGroup.count) * horizontalButtonSpacing
if itemsWidth > width {
groups.append(currentGroup)
currentGroup = []
}
currentGroup.append(i)
}
if !currentGroup.isEmpty {
groups.append(currentGroup)
}
var rowWidths: [CGFloat] = []
for group in groups {
var rowWidth: CGFloat = 0.0
for i in group {
let buttonSize = sizes[i]
rowWidth += buttonSize.width
}
rowWidth += CGFloat(currentGroup.count) * horizontalButtonSpacing
if rowWidth > maxWidth {
maxWidth = rowWidth
}
rowWidths.append(rowWidth)
}
var frames: [CGRect] = []
var originY: CGFloat = 0.0
for i in 0 ..< groups.count {
let rowWidth = rowWidths[i]
var originX = floorToScreenPixels((maxWidth - rowWidth) / 2.0)
for i in groups[i] {
let buttonSize = sizes[i]
frames.append(CGRect(origin: CGPoint(x: originX, y: originY), size: buttonSize))
originX += buttonSize.width + horizontalButtonSpacing
}
originY += buttonHeight + verticalButtonSpacing
}
return (maxWidth, { _ in
var buttonLayoutsAndApply: [(CGSize, () -> PeerButtonNode)] = []
for buttonApply in buttonContinues {
buttonLayoutsAndApply.append(buttonApply(maxWidth))
}
return (CGSize(width: maxWidth, height: originY - verticalButtonSpacing), {
targetNode.buttonNodes = buttonNodes
for i in 0 ..< buttonNodes.count {
let peer = peers[i]
let buttonNode = buttonNodes[i]
buttonNode.pressed = { [weak targetNode] in
targetNode?.openPeer(peer)
}
if buttonNode.supernode == nil {
targetNode.addSubnode(buttonNode)
}
let frame = frames[i]
buttonNode.frame = frame
}
for (_, apply) in buttonLayoutsAndApply {
let _ = apply()
}
return targetNode
})
})
}
}
}
private final class PeerButtonNode: HighlightTrackingButtonNode { private final class PeerButtonNode: HighlightTrackingButtonNode {
private let backgroundNode: ASImageNode private let backgroundNode: ASImageNode
private let textNode: TextNode private let textNode: TextNode

View File

@ -5898,7 +5898,10 @@ public final class EmojiPagerContentComponent: Component {
self.visibleItemSelectionLayers[itemId] = itemSelectionLayer self.visibleItemSelectionLayers[itemId] = itemSelectionLayer
} }
if case .accent = item.tintMode { if case let .custom(color) = item.tintMode {
itemSelectionLayer.backgroundColor = color.withMultipliedAlpha(0.1).cgColor
itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.clear.cgColor
} else if case .accent = item.tintMode {
itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor
itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.clear.cgColor itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.clear.cgColor
} else { } else {

View File

@ -124,6 +124,7 @@ public final class EntityKeyboardComponent: Component {
public let clipContentToTopPanel: Bool public let clipContentToTopPanel: Bool
public let useExternalSearchContainer: Bool public let useExternalSearchContainer: Bool
public let hidePanels: Bool public let hidePanels: Bool
public let customTintColor: UIColor?
public init( public init(
theme: PresentationTheme, theme: PresentationTheme,
@ -157,7 +158,8 @@ public final class EntityKeyboardComponent: Component {
isExpanded: Bool, isExpanded: Bool,
clipContentToTopPanel: Bool, clipContentToTopPanel: Bool,
useExternalSearchContainer: Bool, useExternalSearchContainer: Bool,
hidePanels: Bool = false hidePanels: Bool = false,
customTintColor: UIColor? = nil
) { ) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
@ -191,6 +193,7 @@ public final class EntityKeyboardComponent: Component {
self.clipContentToTopPanel = clipContentToTopPanel self.clipContentToTopPanel = clipContentToTopPanel
self.useExternalSearchContainer = useExternalSearchContainer self.useExternalSearchContainer = useExternalSearchContainer
self.hidePanels = hidePanels self.hidePanels = hidePanels
self.customTintColor = customTintColor
} }
public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool {
@ -257,7 +260,9 @@ public final class EntityKeyboardComponent: Component {
if lhs.useExternalSearchContainer != rhs.useExternalSearchContainer { if lhs.useExternalSearchContainer != rhs.useExternalSearchContainer {
return false return false
} }
if lhs.customTintColor != rhs.customTintColor {
return false
}
return true return true
} }
@ -341,6 +346,7 @@ public final class EntityKeyboardComponent: Component {
icon: icon, icon: icon,
theme: component.theme, theme: component.theme,
useAccentColor: false, useAccentColor: false,
customTintColor: component.customTintColor,
title: title, title: title,
pressed: { [weak self] in pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil) self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil)
@ -376,6 +382,7 @@ public final class EntityKeyboardComponent: Component {
contentTopPanels.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(EntityKeyboardTopPanelComponent( contentTopPanels.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(EntityKeyboardTopPanelComponent(
id: "masks", id: "masks",
theme: component.theme, theme: component.theme,
customTintColor: component.customTintColor,
items: topMaskItems, items: topMaskItems,
containerSideInset: component.containerInsets.left + component.topPanelInsets.left, containerSideInset: component.containerInsets.left + component.topPanelInsets.left,
defaultActiveItemId: maskContent.panelItemGroups.first?.groupId, defaultActiveItemId: maskContent.panelItemGroups.first?.groupId,
@ -430,6 +437,7 @@ public final class EntityKeyboardComponent: Component {
icon: .featured, icon: .featured,
theme: component.theme, theme: component.theme,
useAccentColor: false, useAccentColor: false,
customTintColor: component.customTintColor,
title: component.strings.Stickers_Trending, title: component.strings.Stickers_Trending,
pressed: { [weak self] in pressed: { [weak self] in
self?.component?.stickerContent?.inputInteractionHolder.inputInteraction?.openFeatured?() self?.component?.stickerContent?.inputInteractionHolder.inputInteraction?.openFeatured?()
@ -475,6 +483,7 @@ public final class EntityKeyboardComponent: Component {
icon: icon, icon: icon,
theme: component.theme, theme: component.theme,
useAccentColor: false, useAccentColor: false,
customTintColor: component.customTintColor,
title: title, title: title,
pressed: { [weak self] in pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil)
@ -511,6 +520,7 @@ public final class EntityKeyboardComponent: Component {
contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent(
id: "stickers", id: "stickers",
theme: component.theme, theme: component.theme,
customTintColor: component.customTintColor,
items: topStickerItems, items: topStickerItems,
containerSideInset: component.containerInsets.left + component.topPanelInsets.left, containerSideInset: component.containerInsets.left + component.topPanelInsets.left,
defaultActiveItemId: stickerContent.panelItemGroups.first?.groupId, defaultActiveItemId: stickerContent.panelItemGroups.first?.groupId,
@ -573,6 +583,7 @@ public final class EntityKeyboardComponent: Component {
icon: icon, icon: icon,
theme: component.theme, theme: component.theme,
useAccentColor: false, useAccentColor: false,
customTintColor: component.customTintColor,
title: title, title: title,
pressed: { [weak self] in pressed: { [weak self] in
self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil)
@ -623,6 +634,7 @@ public final class EntityKeyboardComponent: Component {
contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent(
id: "emoji", id: "emoji",
theme: component.theme, theme: component.theme,
customTintColor: component.customTintColor,
items: topEmojiItems, items: topEmojiItems,
containerSideInset: component.containerInsets.left + component.topPanelInsets.left, containerSideInset: component.containerInsets.left + component.topPanelInsets.left,
activeContentItemIdUpdated: emojiContentItemIdUpdated, activeContentItemIdUpdated: emojiContentItemIdUpdated,

View File

@ -281,6 +281,7 @@ final class EntityKeyboardIconTopPanelComponent: Component {
let icon: Icon let icon: Icon
let theme: PresentationTheme let theme: PresentationTheme
let useAccentColor: Bool let useAccentColor: Bool
let customTintColor: UIColor?
let title: String let title: String
let pressed: () -> Void let pressed: () -> Void
@ -288,12 +289,14 @@ final class EntityKeyboardIconTopPanelComponent: Component {
icon: Icon, icon: Icon,
theme: PresentationTheme, theme: PresentationTheme,
useAccentColor: Bool, useAccentColor: Bool,
customTintColor: UIColor?,
title: String, title: String,
pressed: @escaping () -> Void pressed: @escaping () -> Void
) { ) {
self.icon = icon self.icon = icon
self.theme = theme self.theme = theme
self.useAccentColor = useAccentColor self.useAccentColor = useAccentColor
self.customTintColor = customTintColor
self.title = title self.title = title
self.pressed = pressed self.pressed = pressed
} }
@ -308,6 +311,9 @@ final class EntityKeyboardIconTopPanelComponent: Component {
if lhs.useAccentColor != rhs.useAccentColor { if lhs.useAccentColor != rhs.useAccentColor {
return false return false
} }
if lhs.customTintColor != rhs.customTintColor {
return false
}
if lhs.title != rhs.title { if lhs.title != rhs.title {
return false return false
} }
@ -383,14 +389,18 @@ final class EntityKeyboardIconTopPanelComponent: Component {
self.component = component self.component = component
let color: UIColor let color: UIColor
if itemEnvironment.isHighlighted { if let customTintColor = component.customTintColor {
if component.useAccentColor { color = customTintColor
color = component.theme.list.itemAccentColor
} else {
color = component.theme.chat.inputMediaPanel.panelHighlightedIconColor
}
} else { } else {
color = component.theme.chat.inputMediaPanel.panelIconColor if itemEnvironment.isHighlighted {
if component.useAccentColor {
color = component.theme.list.itemAccentColor
} else {
color = component.theme.chat.inputMediaPanel.panelHighlightedIconColor
}
} else {
color = component.theme.chat.inputMediaPanel.panelIconColor
}
} }
if self.iconView.tintColor != color { if self.iconView.tintColor != color {
@ -1192,6 +1202,7 @@ public final class EntityKeyboardTopPanelComponent: Component {
let id: AnyHashable let id: AnyHashable
let theme: PresentationTheme let theme: PresentationTheme
let customTintColor: UIColor?
let items: [Item] let items: [Item]
let containerSideInset: CGFloat let containerSideInset: CGFloat
let defaultActiveItemId: AnyHashable? let defaultActiveItemId: AnyHashable?
@ -1203,6 +1214,7 @@ public final class EntityKeyboardTopPanelComponent: Component {
init( init(
id: AnyHashable, id: AnyHashable,
theme: PresentationTheme, theme: PresentationTheme,
customTintColor: UIColor?,
items: [Item], items: [Item],
containerSideInset: CGFloat, containerSideInset: CGFloat,
defaultActiveItemId: AnyHashable? = nil, defaultActiveItemId: AnyHashable? = nil,
@ -1213,6 +1225,7 @@ public final class EntityKeyboardTopPanelComponent: Component {
) { ) {
self.id = id self.id = id
self.theme = theme self.theme = theme
self.customTintColor = customTintColor
self.items = items self.items = items
self.containerSideInset = containerSideInset self.containerSideInset = containerSideInset
self.defaultActiveItemId = defaultActiveItemId self.defaultActiveItemId = defaultActiveItemId
@ -1229,6 +1242,9 @@ public final class EntityKeyboardTopPanelComponent: Component {
if lhs.theme !== rhs.theme { if lhs.theme !== rhs.theme {
return false return false
} }
if lhs.customTintColor != rhs.customTintColor {
return false
}
if lhs.items != rhs.items { if lhs.items != rhs.items {
return false return false
} }
@ -1847,8 +1863,12 @@ public final class EntityKeyboardTopPanelComponent: Component {
} }
func update(component: EntityKeyboardTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize { func update(component: EntityKeyboardTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
if self.component?.theme !== component.theme { if self.component?.theme !== component.theme || self.component?.customTintColor != component.customTintColor {
self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor if let customTintColor = component.customTintColor {
self.highlightedIconBackgroundView.backgroundColor = customTintColor.withAlphaComponent(0.1)
} else {
self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor
}
} }
self.component = component self.component = component
self.state = state self.state = state

View File

@ -67,7 +67,7 @@ final class ApplyColorFooterItemNode: ItemListControllerFooterItemNode {
super.init() super.init()
self.addSubnode(self.backgroundNode) self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode) // self.addSubnode(self.separatorNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
self.updateItem() self.updateItem()

View File

@ -186,6 +186,7 @@ final class EmojiPickerItemNode: ListViewItemNode {
strings: item.strings, strings: item.strings,
deviceMetrics: .iPhone14ProMax, deviceMetrics: .iPhone14ProMax,
emojiContent: item.emojiContent, emojiContent: item.emojiContent,
backgroundIconColor: item.backgroundIconColor,
backgroundColor: item.theme.list.itemBlocksBackgroundColor, backgroundColor: item.theme.list.itemBlocksBackgroundColor,
separatorColor: item.theme.list.itemBlocksSeparatorColor separatorColor: item.theme.list.itemBlocksSeparatorColor
) )
@ -221,6 +222,7 @@ private final class EmojiSelectionComponent: Component {
public let strings: PresentationStrings public let strings: PresentationStrings
public let deviceMetrics: DeviceMetrics public let deviceMetrics: DeviceMetrics
public let emojiContent: EmojiPagerContentComponent public let emojiContent: EmojiPagerContentComponent
public let backgroundIconColor: UIColor
public let backgroundColor: UIColor public let backgroundColor: UIColor
public let separatorColor: UIColor public let separatorColor: UIColor
@ -229,6 +231,7 @@ private final class EmojiSelectionComponent: Component {
strings: PresentationStrings, strings: PresentationStrings,
deviceMetrics: DeviceMetrics, deviceMetrics: DeviceMetrics,
emojiContent: EmojiPagerContentComponent, emojiContent: EmojiPagerContentComponent,
backgroundIconColor: UIColor,
backgroundColor: UIColor, backgroundColor: UIColor,
separatorColor: UIColor separatorColor: UIColor
) { ) {
@ -236,6 +239,7 @@ private final class EmojiSelectionComponent: Component {
self.strings = strings self.strings = strings
self.deviceMetrics = deviceMetrics self.deviceMetrics = deviceMetrics
self.emojiContent = emojiContent self.emojiContent = emojiContent
self.backgroundIconColor = backgroundIconColor
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
self.separatorColor = separatorColor self.separatorColor = separatorColor
} }
@ -253,6 +257,9 @@ private final class EmojiSelectionComponent: Component {
if lhs.emojiContent != rhs.emojiContent { if lhs.emojiContent != rhs.emojiContent {
return false return false
} }
if lhs.backgroundIconColor != rhs.backgroundIconColor {
return false
}
if lhs.backgroundColor != rhs.backgroundColor { if lhs.backgroundColor != rhs.backgroundColor {
return false return false
} }
@ -338,7 +345,8 @@ private final class EmojiSelectionComponent: Component {
displayBottomPanel: false, displayBottomPanel: false,
isExpanded: true, isExpanded: true,
clipContentToTopPanel: false, clipContentToTopPanel: false,
useExternalSearchContainer: false useExternalSearchContainer: false,
customTintColor: component.backgroundIconColor
)), )),
environment: {}, environment: {},
containerSize: availableSize containerSize: availableSize

View File

@ -252,11 +252,11 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
if let currentItem, currentItem.messageItems.first?.nameColor != item.messageItems.first?.nameColor { if let currentItem, currentItem.messageItems.first?.nameColor != item.messageItems.first?.nameColor || currentItem.messageItems.first?.backgroundEmojiId != item.messageItems.first?.backgroundEmojiId {
if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) { if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) {
snapshot.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: snapshot.frame.size) snapshot.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: snapshot.frame.size)
strongSelf.view.addSubview(snapshot) strongSelf.view.addSubview(snapshot)
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false, completion: { _ in
snapshot.removeFromSuperview() snapshot.removeFromSuperview()
}) })
} }

View File

@ -393,13 +393,18 @@ public func PeerNameColorScreen(
title: buttonTitle, title: buttonTitle,
locked: isLocked, locked: isLocked,
action: { action: {
if isPremium { if !isLocked {
let state = stateValue.with { $0 } let state = stateValue.with { $0 }
let nameColor = state.updatedNameColor ?? peer?.nameColor let nameColor = state.updatedNameColor ?? peer?.nameColor
let backgroundEmojiId = state.updatedBackgroundEmojiId ?? peer?.backgroundEmojiId let backgroundEmojiId = state.updatedBackgroundEmojiId ?? peer?.backgroundEmojiId
let _ = context.engine.accountData.updateNameColorAndEmoji(nameColor: nameColor ?? .blue, backgroundEmojiId: backgroundEmojiId ?? 0).startStandalone() switch subject {
case .account:
let _ = context.engine.accountData.updateNameColorAndEmoji(nameColor: nameColor ?? .blue, backgroundEmojiId: backgroundEmojiId ?? 0).startStandalone()
case let .channel(peerId):
let _ = context.engine.peers.updatePeerNameColorAndEmoji(peerId: peerId, nameColor: nameColor ?? .blue, backgroundEmojiId: backgroundEmojiId ?? 0).startStandalone()
}
dismissImpl?() dismissImpl?()
} else { } else {

View File

@ -836,7 +836,7 @@ final class ShareWithPeersScreenComponent: Component {
guard let stateValue = self.effectiveStateValue else { guard let stateValue = self.effectiveStateValue else {
return return
} }
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset) topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundView.layer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) transition.setTransform(layer: self.backgroundView.layer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
@ -1082,7 +1082,7 @@ final class ShareWithPeersScreenComponent: Component {
self.hapticFeedback.impact(.light) self.hapticFeedback.impact(.light)
} else { } else {
self.postingAvailabilityDisposable.set((component.context.engine.messages.checkStoriesUploadAvailability(target: .peer(peer.id)) self.postingAvailabilityDisposable.set((component.context.engine.messages.checkStoriesUploadAvailability(target: .peer(peer.id))
|> deliverOnMainQueue).start(next: { [weak self] status in |> deliverOnMainQueue).start(next: { [weak self] status in
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
@ -1106,7 +1106,7 @@ final class ShareWithPeersScreenComponent: Component {
previousController.dismiss() previousController.dismiss()
} }
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let controller = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .storiesChannelBoost(peer: peer, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, boosted: false), count: Int32(status.boosts), forceDark: true, cancel: {}, action: { [weak navigationController] in let controller = component.context.sharedContext.makePremiumLimitController(context: component.context, subject: .storiesChannelBoost(peer: peer, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0), count: Int32(status.boosts), forceDark: true, cancel: {}, action: { [weak navigationController] in
UIPasteboard.general.string = link UIPasteboard.general.string = link
if let previousController = navigationController?.viewControllers.reversed().first(where: { $0 is ShareWithPeersScreen}) as? ShareWithPeersScreen { if let previousController = navigationController?.viewControllers.reversed().first(where: { $0 is ShareWithPeersScreen}) as? ShareWithPeersScreen {
@ -1397,7 +1397,7 @@ final class ShareWithPeersScreenComponent: Component {
subtitle = nil subtitle = nil
} }
} }
let isSelected = self.selectedPeers.contains(peer.id) || self.selectedGroups.contains(peer.id) let isSelected = self.selectedPeers.contains(peer.id) || self.selectedGroups.contains(peer.id)
let _ = visibleItem.update( let _ = visibleItem.update(
transition: itemTransition, transition: itemTransition,
@ -1671,7 +1671,21 @@ final class ShareWithPeersScreenComponent: Component {
} }
let fadeTransition = Transition.easeInOut(duration: 0.25) let fadeTransition = Transition.easeInOut(duration: 0.25)
if let searchStateContext = self.searchStateContext, case let .contactsSearch(query, _) = searchStateContext.subject, let value = searchStateContext.stateValue, value.peers.isEmpty {
var searchQuery: String?
var searchResultsAreEmpty = false
if let searchStateContext = self.searchStateContext, let value = searchStateContext.stateValue {
if case let .contactsSearch(query, _) = searchStateContext.subject {
searchQuery = query
} else if case let .members(_, query) = searchStateContext.subject {
searchQuery = query
} else if case let .channels(_, query) = searchStateContext.subject {
searchQuery = query
}
searchResultsAreEmpty = value.peers.isEmpty
}
if let searchQuery, searchResultsAreEmpty {
let sideInset: CGFloat = 44.0 let sideInset: CGFloat = 44.0
let emptyAnimationHeight = 148.0 let emptyAnimationHeight = 148.0
let topInset: CGFloat = topOffset + itemLayout.containerInset + 40.0 let topInset: CGFloat = topOffset + itemLayout.containerInset + 40.0
@ -1695,7 +1709,7 @@ final class ShareWithPeersScreenComponent: Component {
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent( MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.Contacts_Search_NoResultsQueryDescription(query).string, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)), text: .plain(NSAttributedString(string: environment.strings.Contacts_Search_NoResultsQueryDescription(searchQuery).string, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)),
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 0 maximumNumberOfLines: 0
) )
@ -2041,15 +2055,28 @@ final class ShareWithPeersScreenComponent: Component {
containerSize: CGSize(width: containerWidth, height: 1000.0) containerSize: CGSize(width: containerWidth, height: 1000.0)
) )
if !self.navigationTextFieldState.text.isEmpty { let searchQuery = self.navigationTextFieldState.text
if !searchQuery.isEmpty {
var onlyContacts = false var onlyContacts = false
if component.initialPrivacy.base == .closeFriends || component.initialPrivacy.base == .contacts { if component.initialPrivacy.base == .closeFriends || component.initialPrivacy.base == .contacts {
onlyContacts = true onlyContacts = true
} }
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .contactsSearch(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts) {
let searchSubject: ShareWithPeersScreen.StateContext.Subject
switch component.stateContext.subject {
case let .channels(exclude, _):
searchSubject = .channels(exclude: exclude, searchQuery: searchQuery)
case let .members(peerId, _):
searchSubject = .members(peerId: peerId, searchQuery: searchQuery)
default:
searchSubject = .contactsSearch(query: searchQuery, onlyContacts: onlyContacts)
}
if let searchStateContext = self.searchStateContext, searchStateContext.subject == searchSubject {
} else { } else {
self.searchStateDisposable?.dispose() self.searchStateDisposable?.dispose()
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .contactsSearch(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts), editing: false) let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: searchSubject)
var applyState = false var applyState = false
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else { guard let self else {

View File

@ -4,6 +4,7 @@ import TelegramCore
import AccountContext import AccountContext
import TelegramUIPreferences import TelegramUIPreferences
import TemporaryCachedPeerDataManager import TemporaryCachedPeerDataManager
import Postbox
public extension ShareWithPeersScreen { public extension ShareWithPeersScreen {
final class State { final class State {
@ -48,7 +49,7 @@ public extension ShareWithPeersScreen {
case contacts(base: EngineStoryPrivacy.Base) case contacts(base: EngineStoryPrivacy.Base)
case contactsSearch(query: String, onlyContacts: Bool) case contactsSearch(query: String, onlyContacts: Bool)
case members(peerId: EnginePeer.Id, searchQuery: String?) case members(peerId: EnginePeer.Id, searchQuery: String?)
case channels(exclude: Set<EnginePeer.Id>) case channels(exclude: Set<EnginePeer.Id>, searchQuery: String?)
} }
var stateValue: State? var stateValue: State?
@ -584,7 +585,7 @@ public extension ShareWithPeersScreen {
self.stateDisposable = combinedDisposable self.stateDisposable = combinedDisposable
self.listControl = disposableAndLoadMoreControl.1 self.listControl = disposableAndLoadMoreControl.1
case let .channels(excludePeerIds): case let .channels(excludePeerIds, searchQuery):
self.stateDisposable = (combineLatest( self.stateDisposable = (combineLatest(
context.engine.messages.chatList(group: .root, count: 500) |> take(1), context.engine.messages.chatList(group: .root, count: 500) |> take(1),
context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init))) context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init)))
@ -627,13 +628,24 @@ public extension ShareWithPeersScreen {
existingIds.insert(peerId) existingIds.insert(peerId)
} }
} }
let queryTokens = stringIndexTokens(searchQuery ?? "", transliteration: .combined)
func peerMatchesTokens(peer: EnginePeer, tokens: [ValueBoxKey]) -> Bool {
if matchStringIndexTokens(peer.indexName._asIndexName().indexTokens, with: queryTokens) {
return true
}
return false
}
var peers: [EnginePeer] = [] var peers: [EnginePeer] = []
peers = chatList.items.filter { peer in peers = chatList.items.filter { peer in
if let peer = peer.renderedPeer.peer { if let peer = peer.renderedPeer.peer {
if excludePeerIds.contains(peer.id) { if excludePeerIds.contains(peer.id) {
return false return false
} }
if let _ = searchQuery, !peerMatchesTokens(peer: peer, tokens: queryTokens) {
return false
}
if self.initialPeerIds.contains(peer.id) { if self.initialPeerIds.contains(peer.id) {
return false return false
} }

View File

@ -43,6 +43,11 @@ public final class PeerListItemComponent: Component {
case editing(isSelected: Bool, isTinted: Bool) case editing(isSelected: Bool, isTinted: Bool)
} }
public enum SelectionPosition: Equatable {
case left
case right
}
public enum SubtitleAccessory: Equatable { public enum SubtitleAccessory: Equatable {
case none case none
case checks case checks
@ -101,6 +106,8 @@ public final class PeerListItemComponent: Component {
let rightAccessory: RightAccessory let rightAccessory: RightAccessory
let reaction: Reaction? let reaction: Reaction?
let selectionState: SelectionState let selectionState: SelectionState
let selectionPosition: SelectionPosition
let isEnabled: Bool
let hasNext: Bool let hasNext: Bool
let action: (EnginePeer) -> Void let action: (EnginePeer) -> Void
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
@ -121,6 +128,8 @@ public final class PeerListItemComponent: Component {
rightAccessory: RightAccessory = .none, rightAccessory: RightAccessory = .none,
reaction: Reaction? = nil, reaction: Reaction? = nil,
selectionState: SelectionState, selectionState: SelectionState,
selectionPosition: SelectionPosition = .left,
isEnabled: Bool = true,
hasNext: Bool, hasNext: Bool,
action: @escaping (EnginePeer) -> Void, action: @escaping (EnginePeer) -> Void,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil, contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
@ -140,6 +149,8 @@ public final class PeerListItemComponent: Component {
self.rightAccessory = rightAccessory self.rightAccessory = rightAccessory
self.reaction = reaction self.reaction = reaction
self.selectionState = selectionState self.selectionState = selectionState
self.selectionPosition = selectionPosition
self.isEnabled = isEnabled
self.hasNext = hasNext self.hasNext = hasNext
self.action = action self.action = action
self.contextAction = contextAction self.contextAction = contextAction
@ -189,6 +200,12 @@ public final class PeerListItemComponent: Component {
if lhs.selectionState != rhs.selectionState { if lhs.selectionState != rhs.selectionState {
return false return false
} }
if lhs.selectionPosition != rhs.selectionPosition {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.hasNext != rhs.hasNext { if lhs.hasNext != rhs.hasNext {
return false return false
} }
@ -411,6 +428,8 @@ public final class PeerListItemComponent: Component {
self.component = component self.component = component
self.state = state self.state = state
self.containerButton.alpha = component.isEnabled ? 1.0 : 0.3
self.avatarButtonView.isUserInteractionEnabled = component.storyStats != nil && component.openStories != nil self.avatarButtonView.isUserInteractionEnabled = component.storyStats != nil && component.openStories != nil
let labelData: (String, Bool) let labelData: (String, Bool)
@ -457,9 +476,17 @@ public final class PeerListItemComponent: Component {
var avatarLeftInset: CGFloat = component.sideInset + 10.0 var avatarLeftInset: CGFloat = component.sideInset + 10.0
if case let .editing(isSelected, isTinted) = component.selectionState { if case let .editing(isSelected, isTinted) = component.selectionState {
leftInset += 44.0
avatarLeftInset += 44.0
let checkSize: CGFloat = 22.0 let checkSize: CGFloat = 22.0
let checkOriginX: CGFloat
switch component.selectionPosition {
case .left:
leftInset += 44.0
avatarLeftInset += 44.0
checkOriginX = floor((54.0 - checkSize) * 0.5)
case .right:
rightInset += 44.0
checkOriginX = availableSize.width - 11.0 - checkSize
}
let checkLayer: CheckLayer let checkLayer: CheckLayer
if let current = self.checkLayer { if let current = self.checkLayer {
@ -484,7 +511,7 @@ public final class PeerListItemComponent: Component {
checkLayer.setSelected(isSelected, animated: false) checkLayer.setSelected(isSelected, animated: false)
checkLayer.setNeedsDisplay() checkLayer.setNeedsDisplay()
} }
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: checkOriginX, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
} else { } else {
if let checkLayer = self.checkLayer { if let checkLayer = self.checkLayer {
self.checkLayer = nil self.checkLayer = nil

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "avatar_tobedistributed.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,247 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 40.000000 40.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 11.730469 11.000000 cm
0.000000 0.000000 0.000000 scn
12.269731 14.500000 m
12.269731 12.290861 10.478869 10.500000 8.269731 10.500000 c
6.060592 10.500000 4.269731 12.290861 4.269731 14.500000 c
4.269731 16.709139 6.060592 18.500000 8.269731 18.500000 c
10.478869 18.500000 12.269731 16.709139 12.269731 14.500000 c
h
11.296218 7.641908 m
10.421675 7.869528 9.419095 8.000000 8.269734 8.000000 c
3.641746 8.000000 1.393565 5.884617 0.301445 3.829397 c
-0.735194 1.878584 1.060591 0.000000 3.269730 0.000000 c
13.181631 0.000000 l
13.023307 0.328161 12.934570 0.696198 12.934570 1.084961 c
12.934570 1.251627 l
12.934570 1.649643 12.994987 2.011902 13.098507 2.340250 c
13.044313 2.336742 12.989650 2.334961 12.934570 2.334961 c
11.553859 2.334961 10.434570 3.454249 10.434570 4.834961 c
10.434570 5.875430 10.752380 6.841672 11.296218 7.641908 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 24.665039 10.424805 cm
0.000000 0.000000 0.000000 scn
-0.830000 5.410156 m
-0.830000 4.951760 -0.458396 4.580156 0.000000 4.580156 c
0.458396 4.580156 0.830000 4.951760 0.830000 5.410156 c
-0.830000 5.410156 l
h
1.670000 1.660156 m
1.670000 1.201760 2.041604 0.830156 2.500000 0.830156 c
2.958396 0.830156 3.330000 1.201760 3.330000 1.660156 c
1.670000 1.660156 l
h
3.549973 3.140671 m
3.200984 3.893736 l
3.549973 3.140671 l
h
4.170000 5.410156 m
4.170000 4.739607 3.774784 4.159650 3.200984 3.893736 c
3.898962 2.387607 l
5.037511 2.915239 5.830000 4.069291 5.830000 5.410156 c
4.170000 5.410156 l
h
0.830000 5.410156 m
0.830000 6.332472 1.577684 7.080156 2.500000 7.080156 c
2.500000 8.740156 l
0.660892 8.740156 -0.830000 7.249265 -0.830000 5.410156 c
0.830000 5.410156 l
h
2.500000 7.080156 m
3.422316 7.080156 4.170000 6.332472 4.170000 5.410156 c
5.830000 5.410156 l
5.830000 7.249265 4.339108 8.740156 2.500000 8.740156 c
2.500000 7.080156 l
h
1.670000 1.826823 m
1.670000 1.660156 l
3.330000 1.660156 l
3.330000 1.826823 l
1.670000 1.826823 l
h
3.200984 3.893736 m
2.565116 3.599059 1.670000 2.922441 1.670000 1.826823 c
3.330000 1.826823 l
3.330000 1.876959 3.348376 1.955688 3.445795 2.066916 c
3.547248 2.182750 3.706832 2.298569 3.898962 2.387607 c
3.200984 3.893736 l
h
f
n
Q
q
q
1.000000 0.000000 -0.000000 1.000000 26.332031 8.697510 cm
0.000000 0.000000 0.000000 scn
1.566667 1.033366 m
1.566667 0.628357 1.238342 0.300033 0.833333 0.300033 c
0.833333 0.100033 l
1.348799 0.100033 1.766667 0.517900 1.766667 1.033366 c
1.566667 1.033366 l
h
0.833333 0.300033 m
0.428325 0.300033 0.100000 0.628357 0.100000 1.033366 c
-0.100000 1.033366 l
-0.100000 0.517900 0.317868 0.100033 0.833333 0.100033 c
0.833333 0.300033 l
h
0.100000 1.033366 m
0.100000 1.438375 0.428325 1.766699 0.833333 1.766699 c
0.833333 1.966699 l
0.317868 1.966699 -0.100000 1.548832 -0.100000 1.033366 c
0.100000 1.033366 l
h
0.833333 1.766699 m
1.238342 1.766699 1.566667 1.438375 1.566667 1.033366 c
1.766667 1.033366 l
1.766667 1.548832 1.348799 1.966699 0.833333 1.966699 c
0.833333 1.766699 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 26.332031 8.697510 cm
0.000000 0.000000 0.000000 scn
1.666667 1.033366 m
1.666667 0.573129 1.293571 0.200033 0.833333 0.200033 c
0.373096 0.200033 0.000000 0.573129 0.000000 1.033366 c
0.000000 1.493603 0.373096 1.866699 0.833333 1.866699 c
1.293571 1.866699 1.666667 1.493603 1.666667 1.033366 c
h
f
n
Q
Q
endstream
endobj
2 0 obj
3398
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 40.000000 40.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.850980 0.850980 0.850980 scn
40.000000 20.000000 m
40.000000 8.954306 31.045694 0.000000 20.000000 0.000000 c
8.954305 0.000000 0.000000 8.954306 0.000000 20.000000 c
0.000000 31.045696 8.954305 40.000000 20.000000 40.000000 c
31.045694 40.000000 40.000000 31.045696 40.000000 20.000000 c
h
f
n
Q
endstream
endobj
4 0 obj
387
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 40.000000 40.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000003656 00000 n
0000003679 00000 n
0000004314 00000 n
0000004336 00000 n
0000004634 00000 n
0000004736 00000 n
0000004757 00000 n
0000004930 00000 n
0000005004 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
5064
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "avatar_unclaimed.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,106 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 11.730469 11.000000 cm
0.000000 0.000000 0.000000 scn
12.269731 14.500000 m
12.269731 12.290861 10.478869 10.500000 8.269731 10.500000 c
6.060592 10.500000 4.269731 12.290861 4.269731 14.500000 c
4.269731 16.709139 6.060592 18.500000 8.269731 18.500000 c
10.478869 18.500000 12.269731 16.709139 12.269731 14.500000 c
h
10.542313 7.807499 m
9.856975 7.931618 9.101819 8.000000 8.269734 8.000000 c
3.641746 8.000000 1.393565 5.884617 0.301445 3.829397 c
-0.735194 1.878584 1.060591 0.000000 3.269730 0.000000 c
9.769492 0.000000 l
9.769482 0.639818 10.013559 1.279642 10.501725 1.767807 c
11.733958 3.000039 l
10.501725 4.232272 l
9.525414 5.208583 9.525414 6.791495 10.501725 7.767806 c
10.515141 7.781221 10.528671 7.794453 10.542313 7.807499 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 23.169922 10.008057 cm
0.000000 0.000000 0.000000 scn
6.243101 7.578920 m
6.567236 7.903055 7.092763 7.903055 7.416899 7.578920 c
7.741034 7.254785 7.741034 6.729258 7.416899 6.405123 c
5.003798 3.992022 l
7.416899 1.578920 l
7.741034 1.254785 7.741034 0.729258 7.416899 0.405123 c
7.092763 0.080988 6.567236 0.080988 6.243101 0.405123 c
3.830000 2.818224 l
1.416899 0.405123 l
1.092763 0.080988 0.567236 0.080988 0.243101 0.405123 c
-0.081034 0.729258 -0.081034 1.254785 0.243101 1.578920 c
2.656203 3.992022 l
0.243101 6.405123 l
-0.081034 6.729258 -0.081034 7.254785 0.243101 7.578920 c
0.567237 7.903055 1.092763 7.903055 1.416899 7.578920 c
3.830000 5.165819 l
6.243101 7.578920 l
h
f*
n
Q
endstream
endobj
3 0 obj
1555
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 40.000000 40.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001645 00000 n
0000001668 00000 n
0000001841 00000 n
0000001915 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1974
%%EOF

View File

@ -17413,6 +17413,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return .text return .text
}) })
}) })
} else {
self.playShakeAnimation()
} }
case let .withBotStartPayload(botStart): case let .withBotStartPayload(botStart):
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { self.updateChatPresentationInterfaceState(animated: true, interactive: true, {

View File

@ -835,8 +835,8 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
}), nil) }), nil)
} }
}) })
case let .boost(peerId, status, myBoostsStatus): case let .boost(peerId, status, myBoostStatus):
let _ = myBoostsStatus let _ = myBoostStatus
var forceDark = false var forceDark = false
if let updatedPresentationData, updatedPresentationData.initial.theme.overallDarkAppearance { if let updatedPresentationData, updatedPresentationData.initial.theme.overallDarkAppearance {
forceDark = true forceDark = true
@ -847,9 +847,21 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
return return
} }
var isBoosted = false var myBoostCount: Int32 = 0
if status.boostedByMe { var availableBoosts: [MyBoostStatus.Boost] = []
isBoosted = true var occupiedBoosts: [MyBoostStatus.Boost] = []
if let myBoostStatus {
for boost in myBoostStatus.boosts {
if let boostPeer = boost.peer {
if boostPeer.id == peer.id {
myBoostCount += 1
} else {
occupiedBoosts.append(boost)
}
} else {
availableBoosts.append(boost)
}
}
} }
var isCurrent = false var isCurrent = false
@ -861,22 +873,19 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
var currentLevelBoosts = Int32(status.currentLevelBoosts) var currentLevelBoosts = Int32(status.currentLevelBoosts)
var nextLevelBoosts = status.nextLevelBoosts.flatMap(Int32.init) var nextLevelBoosts = status.nextLevelBoosts.flatMap(Int32.init)
if isBoosted && status.boosts == currentLevelBoosts { if myBoostCount > 0 && status.boosts == currentLevelBoosts {
currentLevel = max(0, currentLevel - 1) currentLevel = max(0, currentLevel - 1)
nextLevelBoosts = currentLevelBoosts nextLevelBoosts = currentLevelBoosts
currentLevelBoosts = max(0, currentLevelBoosts - 1) currentLevelBoosts = max(0, currentLevelBoosts - 1)
} }
let subject: PremiumLimitScreen.Subject = .storiesChannelBoost(peer: peer, isCurrent: isCurrent, level: currentLevel, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: nil, boosted: isBoosted) let subject: PremiumLimitScreen.Subject = .storiesChannelBoost(peer: peer, isCurrent: isCurrent, level: currentLevel, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: nil, myBoostCount: myBoostCount)
let nextSubject: PremiumLimitScreen.Subject = .storiesChannelBoost(peer: peer, isCurrent: isCurrent, level: currentLevel, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: nil, boosted: true) let nextSubject: PremiumLimitScreen.Subject = .storiesChannelBoost(peer: peer, isCurrent: isCurrent, level: currentLevel, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: nil, myBoostCount: myBoostCount + 1)
let nextCount = Int32(status.boosts + 1) var nextCount = Int32(status.boosts + 1)
var updateImpl: (() -> Void)? var updateImpl: (() -> Void)?
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
let controller = PremiumLimitScreen(context: context, subject: subject, count: Int32(status.boosts), forceDark: forceDark, action: { let controller = PremiumLimitScreen(context: context, subject: subject, count: Int32(status.boosts), forceDark: forceDark, action: {
if isBoosted {
return true
}
let dismiss = false let dismiss = false
updateImpl?() updateImpl?()
@ -945,8 +954,23 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
updateImpl = { [weak controller] in updateImpl = { [weak controller] in
if let _ = status.nextLevelBoosts { if let _ = status.nextLevelBoosts {
let _ = context.engine.peers.applyChannelBoost(peerId: peerId, slots: []).startStandalone() if let availableBoost = availableBoosts.first {
controller?.updateSubject(nextSubject, count: nextCount) let _ = context.engine.peers.applyChannelBoost(peerId: peerId, slots: [availableBoost.slot]).startStandalone()
controller?.updateSubject(nextSubject, count: nextCount)
availableBoosts.removeFirst()
nextCount += 1
} else if !occupiedBoosts.isEmpty, let myBoostStatus {
let replaceController = ReplaceBoostScreen(context: context, peerId: peerId, myBoostStatus: myBoostStatus, replaceBoosts: { slots in
let _ = context.engine.peers.applyChannelBoost(peerId: peerId, slots: slots).startStandalone()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "\(slots.count) boosts are reassigned from 1 other channel.", timeout: nil), elevatedLayout: true, position: .bottom, action: { _ in return true })
(navigationController?.viewControllers.last as? ViewController)?.present(undoController, in: .window(.root))
})
dismissImpl?()
navigationController?.pushViewController(replaceController)
}
} else { } else {
dismissImpl?() dismissImpl?()
} }
@ -974,7 +998,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
let _ = context.engine.payments.applyPremiumGiftCode(slug: slug).startStandalone() let _ = context.engine.payments.applyPremiumGiftCode(slug: slug).startStandalone()
}, },
openPeer: { peer in openPeer: { peer in
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil)) if peer.id != context.account.peerId {
openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil))
}
}, },
openMessage: { messageId in openMessage: { messageId in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))

View File

@ -8362,7 +8362,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen { if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen {
previousController.dismiss() previousController.dismiss()
} }
let controller = PremiumLimitScreen(context: self.context, subject: .storiesChannelBoost(peer: peer, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, boosted: false), count: Int32(status.boosts), action: { [weak self] in let controller = PremiumLimitScreen(context: self.context, subject: .storiesChannelBoost(peer: peer, isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0), count: Int32(status.boosts), action: { [weak self] in
UIPasteboard.general.string = link UIPasteboard.general.string = link
if let self { if let self {

View File

@ -1795,8 +1795,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .storiesWeekly mappedSubject = .storiesWeekly
case .storiesMonthly: case .storiesMonthly:
mappedSubject = .storiesMonthly mappedSubject = .storiesMonthly
case let .storiesChannelBoost(peer, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, boosted): case let .storiesChannelBoost(peer, isCurrent, level, currentLevelBoosts, nextLevelBoosts, link, myBoostCount):
mappedSubject = .storiesChannelBoost(peer: peer, isCurrent: isCurrent, level: level, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: link, boosted: boosted) mappedSubject = .storiesChannelBoost(peer: peer, isCurrent: isCurrent, level: level, currentLevelBoosts: currentLevelBoosts, nextLevelBoosts: nextLevelBoosts, link: link, myBoostCount: myBoostCount)
} }
return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action)
} }