Various improvements

This commit is contained in:
Ilya Laktyushin 2025-01-27 23:44:07 +04:00
parent 6bf08c72ad
commit 10ac25e1cb
42 changed files with 1966 additions and 1095 deletions

View File

@ -13789,4 +13789,12 @@ Sorry for the inconvenience.";
"Notification.StarsGift.SentSomeone" = "Someone sent you a gift"; "Notification.StarsGift.SentSomeone" = "Someone sent you a gift";
"Notification.StarsGift.UpgradeChannel" = "A gift was turned into a unique collectible"; "Notification.StarsGift.UpgradeChannel" = "A gift was turned into a unique collectible";
"Gift.View.TonGiftInfo" = "This gift is owned by a TON account. [View >]()";
"Gift.View.ViewTonAddressUrl" = "https://tonviewer.com/%@"; "Gift.View.ViewTonAddressUrl" = "https://tonviewer.com/%@";
"Gift.View.CopiedAddress" = "TON address copied to clipboard.";
"NameColor.AddProfileIcons" = "Add Icons To Profile";
"NameColor.AddRepliesIcons" = "Add Icons To Replies";
"NameColor.GiftTitle" = "USE A GIFT";
"NameColor.GiftInfo" = "Apply your collectible's unique look to your profile.";
"NameColor.WearCollectible" = "Wear Collectible";

View File

@ -16,7 +16,7 @@ public protocol ContactSelectionController: ViewController {
public enum ContactSelectionControllerMode { public enum ContactSelectionControllerMode {
case generic case generic
case starsGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, hasActions: Bool, showSelf: Bool) case starsGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, hasActions: Bool, showSelf: Bool, selfSubtitle: String?)
} }
public struct ContactListAdditionalOption: Equatable { public struct ContactListAdditionalOption: Equatable {

View File

@ -573,7 +573,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
index += 1 index += 1
} }
} }
case let .custom(showSelf, sections): case let .custom(showSelf, selfSubtitle, sections):
if !topPeers.isEmpty { if !topPeers.isEmpty {
var index: Int = 0 var index: Int = 0
@ -637,7 +637,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
if showSelf, let accountPeer { if showSelf, let accountPeer {
if let peer = topPeers.first(where: { $0.id == accountPeer.id }) { if let peer = topPeers.first(where: { $0.id == accountPeer.id }) {
let header = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_ThisIsYou.uppercased(), AnyHashable(10)), theme: theme, strings: strings) let header = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_ThisIsYou.uppercased(), AnyHashable(10)), theme: theme, strings: strings)
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, strings.Premium_Gift_ContactSelection_BuySelf)) entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, selfSubtitle))
existingPeerIds.insert(.peer(peer.id)) existingPeerIds.insert(.peer(peer.id))
} }
} }
@ -882,7 +882,7 @@ public enum ContactListPresentation {
public enum TopPeers { public enum TopPeers {
case none case none
case recent case recent
case custom(showSelf: Bool, sections: [(title: String, peerIds: [EnginePeer.Id], hasActions: Bool)]) case custom(showSelf: Bool, selfSubtitle: String?, sections: [(title: String, peerIds: [EnginePeer.Id], hasActions: Bool)])
} }
case orderedByPresence(options: [ContactListAdditionalOption]) case orderedByPresence(options: [ContactListAdditionalOption])
@ -1728,7 +1728,7 @@ public final class ContactListNode: ASDisplayNode {
return .single([]) return .single([])
} }
} }
case let .custom(showSelf, sections): case let .custom(showSelf, _, sections):
var peerIds: [EnginePeer.Id] = [] var peerIds: [EnginePeer.Id] = []
if showSelf { if showSelf {
peerIds.append(context.account.peerId) peerIds.append(context.account.peerId)

View File

@ -2926,6 +2926,10 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
} }
public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) { public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) {
var intrinsicInsets = intrinsicInsets
if intrinsicInsets.top.isZero {
intrinsicInsets.top = statusBarHeight
}
let layout = ContainerViewLayout( let layout = ContainerViewLayout(
size: size, size: size,
metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact, orientation: nil), metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact, orientation: nil),

View File

@ -569,6 +569,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
break; break;
} }
_cancelButton.modernHighlight = false;
[UIView animateWithDuration:0.2f animations:^ [UIView animateWithDuration:0.2f animations:^
{ {
_portraitToolsWrapperView.alpha = 0.0f; _portraitToolsWrapperView.alpha = 0.0f;
@ -688,6 +689,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
[_cropView hideImageForCustomTransition]; [_cropView hideImageForCustomTransition];
[_cropView animateTransitionOutSwitching:false]; [_cropView animateTransitionOutSwitching:false];
_cancelButton.modernHighlight = false;
[_cancelButton.layer removeAllAnimations];
_previewView.hidden = true; _previewView.hidden = true;
[UIView animateWithDuration:0.3f animations:^ [UIView animateWithDuration:0.3f animations:^
{ {

View File

@ -1507,9 +1507,9 @@
doneButtonType = TGPhotoEditorDoneButtonDone; doneButtonType = TGPhotoEditorDoneButtonDone;
if (sideButtonsHiddenInCrop) { if (sideButtonsHiddenInCrop) {
[_portraitToolbarView setCancelDoneButtonsHidden:true animated:true]; [_portraitToolbarView setCancelDoneButtonsHidden:true animated:false];
[_portraitToolbarView setCenterButtonsHidden:false animated:true]; [_portraitToolbarView setCenterButtonsHidden:false animated:false];
[_landscapeToolbarView setAllButtonsHidden:false animated:true]; [_landscapeToolbarView setAllButtonsHidden:false animated:false];
} else { } else {
[_portraitToolbarView setAllButtonsHidden:false animated:false]; [_portraitToolbarView setAllButtonsHidden:false animated:false];
[_landscapeToolbarView setAllButtonsHidden:false animated:false]; [_landscapeToolbarView setAllButtonsHidden:false animated:false];

View File

@ -514,7 +514,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
}, openWallpaperSettings: { }, openWallpaperSettings: {
pushControllerImpl?(ThemeGridController(context: context)) pushControllerImpl?(ThemeGridController(context: context))
}, openNameColorSettings: { }, openNameColorSettings: {
pushControllerImpl?(PeerNameColorScreen(context: context, subject: .account)) pushControllerImpl?(UserAppearanceScreen(context: context))
}, selectAccentColor: { accentColor in }, selectAccentColor: { accentColor in
selectAccentColorImpl?(accentColor) selectAccentColorImpl?(accentColor)
}, openAccentColorPicker: { themeReference, create in }, openAccentColorPicker: { themeReference, create in

View File

@ -1124,6 +1124,9 @@ private final class ProfileGiftsContextImpl {
} }
if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) { if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) {
self.filteredGifts[index] = self.filteredGifts[index].withSavedToProfile(added) self.filteredGifts[index] = self.filteredGifts[index].withSavedToProfile(added)
if !self.filter.contains(.hidden) && !added {
self.filteredGifts.remove(at: index)
}
} }
self.pushState() self.pushState()
} }
@ -1803,3 +1806,14 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer
|> ignoreValues |> ignoreValues
} }
} }
public extension StarGift.UniqueGift {
var itemFile: TelegramMediaFile? {
for attribute in self.attributes {
if case let .model(_, file, _) = attribute {
return file
}
}
return nil
}
}

View File

@ -0,0 +1,18 @@
import Foundation
import TelegramCore
public func hasBirthdayToday(cachedData: CachedUserData) -> Bool {
if let birthday = cachedData.birthday {
return hasBirthdayToday(birthday: birthday)
}
return false
}
public func hasBirthdayToday(birthday: TelegramBirthday) -> Bool {
let today = Calendar.current.dateComponents(Set([.day, .month]), from: Date())
if today.day == Int(birthday.day) && today.month == Int(birthday.month) {
return true
}
return false
}

View File

@ -315,7 +315,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private var displayedGiftTooltip = false private var displayedGiftTooltip = false
private func presentGiftTooltip() { private func presentGiftTooltip() {
guard let context = self.context, !self.displayedGiftTooltip else { guard let context = self.context, !self.displayedGiftTooltip, let parentController = self.interfaceInteraction?.chatController() else {
return return
} }
self.displayedGiftTooltip = true self.displayedGiftTooltip = true
@ -332,7 +332,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start() let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start()
Queue.mainQueue().after(0.4, { Queue.mainQueue().after(0.4, {
let absoluteFrame = self.giftButton.view.convert(self.giftButton.bounds, to: nil) let absoluteFrame = self.giftButton.view.convert(self.giftButton.bounds, to: parentController.view)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 11.0), size: CGSize()) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY + 11.0), size: CGSize())
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }

View File

@ -134,11 +134,15 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
if let peerStatus = title.peerStatus { if let peerStatus = title.peerStatus {
let statusContent: EmojiStatusComponent.Content let statusContent: EmojiStatusComponent.Content
var statusParticleColor: UIColor?
switch peerStatus { switch peerStatus {
case .premium: case .premium:
statusContent = .premium(color: self.theme.list.itemAccentColor) statusContent = .premium(color: self.theme.list.itemAccentColor)
case let .emoji(emoji): case let .emoji(emojiStatus):
statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) statusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2))
if let color = emojiStatus.color {
statusParticleColor = UIColor(rgb: UInt32(bitPattern: color))
}
} }
var titleCredibilityIconTransition: ComponentTransition var titleCredibilityIconTransition: ComponentTransition
@ -164,6 +168,7 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
content: statusContent, content: statusContent,
particleColor: statusParticleColor,
isVisibleForAnimations: true, isVisibleForAnimations: true,
action: { [weak self] in action: { [weak self] in
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else {

View File

@ -151,7 +151,7 @@ private enum ChatTitleCredibilityIcon: Equatable {
case scam case scam
case verified case verified
case premium case premium
case emojiStatus(PeerEmojiStatus, Int32?) case emojiStatus(PeerEmojiStatus)
} }
public final class ChatTitleView: UIView, NavigationBarTitleView { public final class ChatTitleView: UIView, NavigationBarTitleView {
@ -275,7 +275,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
} else if peer.isScam { } else if peer.isScam {
titleCredibilityIcon = .scam titleCredibilityIcon = .scam
} else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled { } else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled {
titleStatusIcon = .emojiStatus(emojiStatus, emojiStatus.color) titleStatusIcon = .emojiStatus(emojiStatus)
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
titleCredibilityIcon = .premium titleCredibilityIcon = .premium
} }
@ -284,7 +284,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
titleCredibilityIcon = .verified titleCredibilityIcon = .verified
} }
if let verificationIconFileId = peer.verificationIconFileId { if let verificationIconFileId = peer.verificationIconFileId {
titleVerifiedIcon = .emojiStatus(PeerEmojiStatus(content: .emoji(fileId: verificationIconFileId), expirationDate: nil), nil) titleVerifiedIcon = .emojiStatus(PeerEmojiStatus(content: .emoji(fileId: verificationIconFileId), expirationDate: nil))
} }
} }
} }
@ -839,7 +839,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
titleCredibilityContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_FakeAccount.uppercased()) titleCredibilityContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_FakeAccount.uppercased())
case .scam: case .scam:
titleCredibilityContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_ScamAccount.uppercased()) titleCredibilityContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_ScamAccount.uppercased())
case let .emojiStatus(emojiStatus, _): case let .emojiStatus(emojiStatus):
titleCredibilityContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) titleCredibilityContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2))
} }
@ -855,16 +855,16 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
titleVerifiedContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_FakeAccount.uppercased()) titleVerifiedContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_FakeAccount.uppercased())
case .scam: case .scam:
titleVerifiedContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_ScamAccount.uppercased()) titleVerifiedContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_ScamAccount.uppercased())
case let .emojiStatus(emojiStatus, _): case let .emojiStatus(emojiStatus):
titleVerifiedContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) titleVerifiedContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2))
} }
let titleStatusContent: EmojiStatusComponent.Content let titleStatusContent: EmojiStatusComponent.Content
var titleStatusParticleColor: UIColor? var titleStatusParticleColor: UIColor?
switch self.titleStatusIcon { switch self.titleStatusIcon {
case let .emojiStatus(emojiStatus, color): case let .emojiStatus(emojiStatus):
titleStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2)) titleStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor, themeColor: self.theme.list.itemAccentColor, loopMode: .count(2))
if let color { if let color = emojiStatus.color {
titleStatusParticleColor = UIColor(rgb: UInt32(bitPattern: color)) titleStatusParticleColor = UIColor(rgb: UInt32(bitPattern: color))
} }
default: default:

View File

@ -1149,7 +1149,7 @@ public final class EmojiStatusSelectionController: ViewController {
} }
} }
if let previewItem = self.previewItem, let itemFile = previewItem.item.itemFile { if let previewItem = self.previewItem, let itemFile = previewItem.item.displayFile {
let previewScreenView: ComponentView<Empty> let previewScreenView: ComponentView<Empty>
var previewScreenTransition = transition var previewScreenTransition = transition
if let current = self.previewScreenView { if let current = self.previewScreenView {
@ -1190,7 +1190,16 @@ public final class EmojiStatusSelectionController: ViewController {
return return
} }
if let result = result, let previewItem = strongSelf.previewItem { if let result, let previewItem = strongSelf.previewItem {
let expirationDate: Int32? = result.timestamp
if let itemGift = previewItem.item.itemGift {
let _ = (strongSelf.context.engine.accountData.setStarGiftStatus(starGift: itemGift, expirationDate: expirationDate)
|> deliverOnMainQueue).start()
if let destinationView = strongSelf.controller?.destinationItemView() {
strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: nil, destinationView: destinationView, fromBackground: true)
}
} else {
var emojiString: String? var emojiString: String?
if let itemFile = previewItem.item.itemFile { if let itemFile = previewItem.item.itemFile {
attributeLoop: for attribute in itemFile.attributes { attributeLoop: for attribute in itemFile.attributes {
@ -1235,13 +1244,12 @@ public final class EmojiStatusSelectionController: ViewController {
return return
} }
let expirationDate: Int32? = result.timestamp
let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate) let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate)
|> deliverOnMainQueue).start() |> deliverOnMainQueue).start()
strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: filePath, destinationView: destinationView, fromBackground: true) strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: filePath, destinationView: destinationView, fromBackground: true)
}) })
}
} else { } else {
strongSelf.dismissedPreviewItem = strongSelf.previewItem strongSelf.dismissedPreviewItem = strongSelf.previewItem
strongSelf.previewItem = nil strongSelf.previewItem = nil
@ -1551,3 +1559,15 @@ private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to tar
return keyframes return keyframes
} }
extension EmojiPagerContentComponent.Item {
var displayFile: TelegramMediaFile? {
if let file = self.itemFile {
return file
} else if let gift = self.itemGift {
return gift.itemFile
} else {
return nil
}
}
}

View File

@ -572,14 +572,16 @@ public final class EntityKeyboardComponent: Component {
for itemGroup in emojiContent.panelItemGroups { for itemGroup in emojiContent.panelItemGroups {
if !itemGroup.items.isEmpty { if !itemGroup.items.isEmpty {
if let id = itemGroup.groupId.base as? String, id != "peerSpecific" { if let id = itemGroup.groupId.base as? String, id != "peerSpecific" {
if id == "recent" || id == "liked" { if id == "recent" || id == "liked" || id == "collectible" {
let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [
"recent": .recent, "recent": .recent,
"liked": .liked, "liked": .liked,
"collectible": .collectible
] ]
let titleMapping: [String: String] = [ let titleMapping: [String: String] = [
"recent": component.strings.Stickers_Recent, "recent": component.strings.Stickers_Recent,
"liked": "", "liked": "",
"collectible": ""
] ]
if let icon = iconMapping[id], let title = titleMapping[id] { if let icon = iconMapping[id], let title = titleMapping[id] {
topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( topEmojiItems.append(EntityKeyboardTopPanelComponent.Item(

View File

@ -277,6 +277,7 @@ final class EntityKeyboardIconTopPanelComponent: Component {
case saved case saved
case premium case premium
case liked case liked
case collectible
} }
let icon: Icon let icon: Icon
@ -363,6 +364,8 @@ final class EntityKeyboardIconTopPanelComponent: Component {
image = UIImage(bundleImageName: "Chat/Input/Media/PanelSavedIcon") image = UIImage(bundleImageName: "Chat/Input/Media/PanelSavedIcon")
case .liked: case .liked:
image = UIImage(bundleImageName: "Chat/Input/Media/PanelHeartIcon")?.withRenderingMode(.alwaysTemplate) image = UIImage(bundleImageName: "Chat/Input/Media/PanelHeartIcon")?.withRenderingMode(.alwaysTemplate)
case .collectible:
image = UIImage(bundleImageName: "Chat/Input/Media/PanelCollectibleIcon")?.withRenderingMode(.alwaysTemplate)
case .premium: case .premium:
image = generateImage(CGSize(width: 44.0, height: 44.0), contextGenerator: { size, context in image = generateImage(CGSize(width: 44.0, height: 44.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))

View File

@ -87,6 +87,7 @@ public final class GiftItemComponent: Component {
case profile case profile
case thumbnail case thumbnail
case preview case preview
case grid
} }
let context: AccountContext let context: AccountContext
@ -99,6 +100,7 @@ public final class GiftItemComponent: Component {
let isLoading: Bool let isLoading: Bool
let isHidden: Bool let isHidden: Bool
let isSoldOut: Bool let isSoldOut: Bool
let isSelected: Bool
let mode: Mode let mode: Mode
public init( public init(
@ -112,6 +114,7 @@ public final class GiftItemComponent: Component {
isLoading: Bool = false, isLoading: Bool = false,
isHidden: Bool = false, isHidden: Bool = false,
isSoldOut: Bool = false, isSoldOut: Bool = false,
isSelected: Bool = false,
mode: Mode = .generic mode: Mode = .generic
) { ) {
self.context = context self.context = context
@ -124,6 +127,7 @@ public final class GiftItemComponent: Component {
self.isLoading = isLoading self.isLoading = isLoading
self.isHidden = isHidden self.isHidden = isHidden
self.isSoldOut = isSoldOut self.isSoldOut = isSoldOut
self.isSelected = isSelected
self.mode = mode self.mode = mode
} }
@ -158,6 +162,9 @@ public final class GiftItemComponent: Component {
if lhs.isSoldOut != rhs.isSoldOut { if lhs.isSoldOut != rhs.isSoldOut {
return false return false
} }
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.mode != rhs.mode { if lhs.mode != rhs.mode {
return false return false
} }
@ -181,6 +188,7 @@ public final class GiftItemComponent: Component {
private let ribbonText = ComponentView<Empty>() private let ribbonText = ComponentView<Empty>()
private var animationLayer: InlineStickerItemLayer? private var animationLayer: InlineStickerItemLayer?
private var selectionLayer: SimpleShapeLayer?
private var disposables = DisposableSet() private var disposables = DisposableSet()
private var fetchedFiles = Set<Int64>() private var fetchedFiles = Set<Int64>()
@ -234,6 +242,10 @@ public final class GiftItemComponent: Component {
size = CGSize(width: availableSize.width, height: availableSize.width) size = CGSize(width: availableSize.width, height: availableSize.width)
iconSize = CGSize(width: floor(size.width * 0.7), height: floor(size.width * 0.7)) iconSize = CGSize(width: floor(size.width * 0.7), height: floor(size.width * 0.7))
cornerRadius = floor(availableSize.width * 0.2) cornerRadius = floor(availableSize.width * 0.2)
case .grid:
size = CGSize(width: availableSize.width, height: availableSize.width)
iconSize = CGSize(width: floor(size.width * 0.7), height: floor(size.width * 0.7))
cornerRadius = 10.0
case .preview: case .preview:
size = availableSize size = availableSize
iconSize = CGSize(width: floor(size.width * 0.6), height: floor(size.width * 0.6)) iconSize = CGSize(width: floor(size.width * 0.6), height: floor(size.width * 0.6))
@ -587,6 +599,43 @@ public final class GiftItemComponent: Component {
} }
} }
if case .grid = component.mode {
let lineWidth: CGFloat = 2.0
let selectionFrame = CGRect(origin: .zero, size: size).insetBy(dx: 3.0, dy: 3.0)
if component.isSelected {
let selectionLayer: SimpleShapeLayer
if let current = self.selectionLayer {
selectionLayer = current
} else {
selectionLayer = SimpleShapeLayer()
self.selectionLayer = selectionLayer
self.layer.addSublayer(selectionLayer)
selectionLayer.fillColor = UIColor.clear.cgColor
selectionLayer.strokeColor = UIColor.white.cgColor
selectionLayer.lineWidth = lineWidth
selectionLayer.frame = selectionFrame
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
if !transition.animation.isImmediate {
let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
}
}
} else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil
let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
selectionLayer.removeFromSuperlayer()
})
}
}
return size return size
} }
} }

View File

@ -19,7 +19,6 @@ import TelegramStringFormatting
import PlainButtonComponent import PlainButtonComponent
import BlurredBackgroundComponent import BlurredBackgroundComponent
import PremiumStarComponent import PremiumStarComponent
import ConfettiEffect
import TextFormat import TextFormat
import GiftItemComponent import GiftItemComponent
import InAppPurchaseManager import InAppPurchaseManager

View File

@ -47,6 +47,7 @@ swift_library(
"//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BlurredBackgroundComponent",
"//submodules/ProgressNavigationButtonNode", "//submodules/ProgressNavigationButtonNode",
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen", "//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
"//submodules/ConfettiEffect",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -233,7 +233,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
case let .starGift(gift): case let .starGift(gift):
media = [ media = [
TelegramMediaAction( TelegramMediaAction(
action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: true, upgradeStars: item.upgradeStars, isRefunded: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil) action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil)
) )
] ]
} }

View File

@ -35,6 +35,7 @@ import ProgressNavigationButtonNode
import Markdown import Markdown
import GiftViewScreen import GiftViewScreen
import UndoUI import UndoUI
import ConfettiEffect
final class GiftSetupScreenComponent: Component { final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -377,6 +378,8 @@ final class GiftSetupScreenComponent: Component {
action: { _ in return true } action: { _ in return true }
) )
(navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current) (navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current)
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
} }
if let completion { if let completion {

View File

@ -47,6 +47,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let cancel: (Bool) -> Void let cancel: (Bool) -> Void
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let openAddress: (String) -> Void let openAddress: (String) -> Void
let copyAddress: (String) -> Void
let updateSavedToProfile: (Bool) -> Void let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void let convertToStars: () -> Void
let openStarsIntro: () -> Void let openStarsIntro: () -> Void
@ -66,6 +67,7 @@ private final class GiftViewSheetContent: CombinedComponent {
cancel: @escaping (Bool) -> Void, cancel: @escaping (Bool) -> Void,
openPeer: @escaping (EnginePeer) -> Void, openPeer: @escaping (EnginePeer) -> Void,
openAddress: @escaping (String) -> Void, openAddress: @escaping (String) -> Void,
copyAddress: @escaping (String) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void, updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void, convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void, openStarsIntro: @escaping () -> Void,
@ -84,6 +86,7 @@ private final class GiftViewSheetContent: CombinedComponent {
self.cancel = cancel self.cancel = cancel
self.openPeer = openPeer self.openPeer = openPeer
self.openAddress = openAddress self.openAddress = openAddress
self.copyAddress = copyAddress
self.updateSavedToProfile = updateSavedToProfile self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro self.openStarsIntro = openStarsIntro
@ -447,6 +450,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var soldOut = false var soldOut = false
var nameHidden = false var nameHidden = false
var upgraded = false var upgraded = false
var exported = false
var canUpgrade = false var canUpgrade = false
var upgradeStars: Int64? var upgradeStars: Int64?
var uniqueGift: StarGift.UniqueGift? var uniqueGift: StarGift.UniqueGift?
@ -1082,6 +1086,8 @@ private final class GiftViewSheetContent: CombinedComponent {
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents) return (TelegramTextAttributes.URL, contents)
}) })
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
@ -1286,6 +1292,8 @@ private final class GiftViewSheetContent: CombinedComponent {
) )
)) ))
case let .address(address): case let .address(address):
exported = true
func formatAddress(_ str: String) -> String { func formatAddress(_ str: String) -> String {
var result = str var result = str
let middleIndex = result.index(result.startIndex, offsetBy: str.count / 2) let middleIndex = result.index(result.startIndex, offsetBy: str.count / 2)
@ -1302,8 +1310,7 @@ private final class GiftViewSheetContent: CombinedComponent {
MultilineTextComponent(text: .plain(NSAttributedString(string: formatAddress(address), font: tableLargeMonospaceFont, textColor: tableLinkColor)), maximumNumberOfLines: 2, lineSpacing: 0.2) MultilineTextComponent(text: .plain(NSAttributedString(string: formatAddress(address), font: tableLargeMonospaceFont, textColor: tableLinkColor)), maximumNumberOfLines: 2, lineSpacing: 0.2)
), ),
action: { action: {
component.openAddress(address) component.copyAddress(address)
component.cancel(true)
} }
) )
) )
@ -1403,6 +1410,8 @@ private final class GiftViewSheetContent: CombinedComponent {
var canTransfer = true var canTransfer = true
if let peer = state.peerMap[peerId], case let .channel(channel) = peer, !channel.flags.contains(.isCreator) { if let peer = state.peerMap[peerId], case let .channel(channel) = peer, !channel.flags.contains(.isCreator) {
canTransfer = false canTransfer = false
} else if subject.arguments?.transferStars == nil {
canTransfer = false
} }
let buttonsCount = canTransfer ? 3 : 2 let buttonsCount = canTransfer ? 3 : 2
@ -1871,13 +1880,17 @@ private final class GiftViewSheetContent: CombinedComponent {
originY += table.size.height + 23.0 originY += table.size.height + 23.0
} }
if incoming && !converted && !upgraded && !showUpgradePreview && !showWearPreview { if ((incoming && !converted && !upgraded) || exported) && (!showUpgradePreview && !showWearPreview) {
let linkColor = theme.actionSheet.controlAccentColor let linkColor = theme.actionSheet.controlAccentColor
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
} }
let descriptionText: String var addressToOpen: String?
if savedToProfile { var descriptionText: String
if let uniqueGift, case let .address(address) = uniqueGift.owner {
addressToOpen = address
descriptionText = strings.Gift_View_TonGiftInfo
} else if savedToProfile {
descriptionText = isChannelGift ? strings.Gift_View_DisplayedInfoHide_Channel : strings.Gift_View_DisplayedInfoHide descriptionText = isChannelGift ? strings.Gift_View_DisplayedInfoHide_Channel : strings.Gift_View_DisplayedInfoHide
} else if let upgradeStars, upgradeStars > 0 && !upgraded { } else if let upgradeStars, upgradeStars > 0 && !upgraded {
descriptionText = isChannelGift ? strings.Gift_View_HiddenInfoShow_Channel : strings.Gift_View_HiddenInfoShow descriptionText = isChannelGift ? strings.Gift_View_HiddenInfoShow_Channel : strings.Gift_View_HiddenInfoShow
@ -1894,6 +1907,8 @@ private final class GiftViewSheetContent: CombinedComponent {
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents) return (TelegramTextAttributes.URL, contents)
}) })
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
@ -1917,12 +1932,17 @@ private final class GiftViewSheetContent: CombinedComponent {
}, },
tapAction: { attributes, _ in tapAction: { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
if let addressToOpen {
component.openAddress(addressToOpen)
component.cancel(true)
} else {
component.updateSavedToProfile(!savedToProfile) component.updateSavedToProfile(!savedToProfile)
Queue.mainQueue().after(0.6, { Queue.mainQueue().after(0.6, {
component.cancel(false) component.cancel(false)
}) })
} }
} }
}
), ),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate transition: .immediate
@ -2201,6 +2221,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
let subject: GiftViewScreen.Subject let subject: GiftViewScreen.Subject
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let openAddress: (String) -> Void let openAddress: (String) -> Void
let copyAddress: (String) -> Void
let updateSavedToProfile: (Bool) -> Void let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void let convertToStars: () -> Void
let openStarsIntro: () -> Void let openStarsIntro: () -> Void
@ -2218,6 +2239,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
subject: GiftViewScreen.Subject, subject: GiftViewScreen.Subject,
openPeer: @escaping (EnginePeer) -> Void, openPeer: @escaping (EnginePeer) -> Void,
openAddress: @escaping (String) -> Void, openAddress: @escaping (String) -> Void,
copyAddress: @escaping (String) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void, updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void, convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void, openStarsIntro: @escaping () -> Void,
@ -2234,6 +2256,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
self.subject = subject self.subject = subject
self.openPeer = openPeer self.openPeer = openPeer
self.openAddress = openAddress self.openAddress = openAddress
self.copyAddress = copyAddress
self.updateSavedToProfile = updateSavedToProfile self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro self.openStarsIntro = openStarsIntro
@ -2286,6 +2309,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
}, },
openPeer: context.component.openPeer, openPeer: context.component.openPeer,
openAddress: context.component.openAddress, openAddress: context.component.openAddress,
copyAddress: context.component.copyAddress,
updateSavedToProfile: context.component.updateSavedToProfile, updateSavedToProfile: context.component.updateSavedToProfile,
convertToStars: context.component.convertToStars, convertToStars: context.component.convertToStars,
openStarsIntro: context.component.openStarsIntro, openStarsIntro: context.component.openStarsIntro,
@ -2447,6 +2471,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
var openPeerImpl: ((EnginePeer) -> Void)? var openPeerImpl: ((EnginePeer) -> Void)?
var openAddressImpl: ((String) -> Void)? var openAddressImpl: ((String) -> Void)?
var copyAddressImpl: ((String) -> Void)?
var updateSavedToProfileImpl: ((Bool) -> Void)? var updateSavedToProfileImpl: ((Bool) -> Void)?
var convertToStarsImpl: (() -> Void)? var convertToStarsImpl: (() -> Void)?
var openStarsIntroImpl: (() -> Void)? var openStarsIntroImpl: (() -> Void)?
@ -2470,6 +2495,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
openAddress: { address in openAddress: { address in
openAddressImpl?(address) openAddressImpl?(address)
}, },
copyAddress: { address in
copyAddressImpl?(address)
},
updateSavedToProfile: { added in updateSavedToProfile: { added in
updateSavedToProfileImpl?(added) updateSavedToProfileImpl?(added)
}, },
@ -2537,6 +2565,18 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} }
} }
} }
copyAddressImpl = { [weak self] address in
guard let self else {
return
}
UIPasteboard.general.string = address
self.dismissAllTooltips()
self.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Gift_View_CopiedAddress), elevatedLayout: false, position: .bottom, action: { _ in return true }), in: .current)
HapticFeedback().tap()
}
updateSavedToProfileImpl = { [weak self] added in updateSavedToProfileImpl = { [weak self] added in
guard let self, let arguments = self.subject.arguments, let reference = arguments.reference else { guard let self, let arguments = self.subject.arguments, let reference = arguments.reference else {
return return
@ -2618,6 +2658,11 @@ public class GiftViewScreen: ViewControllerComponentContainer {
let configuration = GiftConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let configuration = GiftConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let starsConvertMaxDate = arguments.date + configuration.convertToStarsPeriod let starsConvertMaxDate = arguments.date + configuration.convertToStarsPeriod
var isChannelGift = false
if case let .peer(peerId, _) = reference, peerId.namespace == Namespaces.Peer.CloudChannel {
isChannelGift = true
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if currentTime > starsConvertMaxDate { if currentTime > starsConvertMaxDate {
let days: Int32 = Int32(ceil(Float(configuration.convertToStarsPeriod) / 86400.0)) let days: Int32 = Int32(ceil(Float(configuration.convertToStarsPeriod) / 86400.0))
@ -2653,9 +2698,11 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if let navigationController { if let navigationController {
Queue.mainQueue().after(0.5) { Queue.mainQueue().after(0.5) {
if !isChannelGift {
if let starsContext = context.starsContext { if let starsContext = context.starsContext {
navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true) navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true)
} }
}
if let lastController = navigationController.viewControllers.last as? ViewController { if let lastController = navigationController.viewControllers.last as? ViewController {
let resultController = UndoOverlayController( let resultController = UndoOverlayController(

View File

@ -127,7 +127,7 @@ final class MediaEditorComposer {
if values.isSticker { if values.isSticker {
self.maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) self.maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97)))
} else if values.isAvatar { } else if values.isAvatar {
self.maskImage = rectangleMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) self.maskImage = rectangleMaskImage(size: CGSize(width: 1080.0, height: 1080.0))
} }
if let drawing = values.drawing, let drawingImage = CIImage(image: drawing, options: [.colorSpace: self.colorSpace]) { if let drawing = values.drawing, let drawingImage = CIImage(image: drawing, options: [.colorSpace: self.colorSpace]) {
@ -227,7 +227,7 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp
if values.isSticker { if values.isSticker {
maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97)))
} else if values.isAvatar { } else if values.isAvatar {
maskImage = rectangleMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) maskImage = rectangleMaskImage(size: CGSize(width: 1080.0, height: 1080.0))
} else if let outputDimensions { } else if let outputDimensions {
maskImage = rectangleMaskImage(size: outputDimensions.aspectFitted(CGSize(width: 1080.0, height: 1080.0))) maskImage = rectangleMaskImage(size: outputDimensions.aspectFitted(CGSize(width: 1080.0, height: 1080.0)))
} }
@ -299,10 +299,14 @@ private func makeEditorImageFrameComposition(context: CIContext, inputImage: CII
} }
resultImage = resultImage.transformed(by: CGAffineTransform(translationX: dimensions.width / 2.0, y: dimensions.height / 2.0)) resultImage = resultImage.transformed(by: CGAffineTransform(translationX: dimensions.width / 2.0, y: dimensions.height / 2.0))
if values.isSticker || values.isAvatar { if values.isSticker {
let minSize = min(dimensions.width, dimensions.height) let minSize = min(dimensions.width, dimensions.height)
let scaledSize = CGSize(width: floor(minSize * 0.97), height: floor(minSize * 0.97)) let scaledSize = CGSize(width: floor(minSize * 0.97), height: floor(minSize * 0.97))
resultImage = resultImage.transformed(by: CGAffineTransform(translationX: -(dimensions.width - scaledSize.width) / 2.0, y: -(dimensions.height - scaledSize.height) / 2.0)).cropped(to: CGRect(origin: .zero, size: scaledSize)) resultImage = resultImage.transformed(by: CGAffineTransform(translationX: -(dimensions.width - scaledSize.width) / 2.0, y: -(dimensions.height - scaledSize.height) / 2.0)).cropped(to: CGRect(origin: .zero, size: scaledSize))
} else if values.isAvatar {
let minSize = min(dimensions.width, dimensions.height)
let scaledSize = CGSize(width: minSize, height: minSize)
resultImage = resultImage.transformed(by: CGAffineTransform(translationX: -(dimensions.width - scaledSize.width) / 2.0, y: -(dimensions.height - scaledSize.height) / 2.0)).cropped(to: CGRect(origin: .zero, size: scaledSize))
} else if values.isCover, let outputDimensions { } else if values.isCover, let outputDimensions {
let minSize = min(dimensions.width, dimensions.height) let minSize = min(dimensions.width, dimensions.height)
let scaledSize = outputDimensions.aspectFitted(CGSize(width: minSize, height: minSize)) let scaledSize = outputDimensions.aspectFitted(CGSize(width: minSize, height: minSize))

View File

@ -5950,7 +5950,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
if let stickerBackgroundView = self.stickerBackgroundView, let stickerOverlayLayer = self.stickerOverlayLayer, let stickerFrameLayer = self.stickerFrameLayer { if let stickerBackgroundView = self.stickerBackgroundView, let stickerOverlayLayer = self.stickerOverlayLayer, let stickerFrameLayer = self.stickerFrameLayer {
let stickerFrameFraction: CGFloat let stickerFrameFraction: CGFloat
switch controller.mode { switch controller.mode {
case .avatarEditor, .stickerEditor: case .stickerEditor:
stickerFrameFraction = 0.97 stickerFrameFraction = 0.97
default: default:
stickerFrameFraction = 1.0 stickerFrameFraction = 1.0

View File

@ -813,6 +813,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
do { do {
self.currentCredibilityIcon = credibilityIcon self.currentCredibilityIcon = credibilityIcon
var emojiStatusSize: CGSize?
var currentEmojiStatus: PeerEmojiStatus? var currentEmojiStatus: PeerEmojiStatus?
let emojiRegularStatusContent: EmojiStatusComponent.Content let emojiRegularStatusContent: EmojiStatusComponent.Content
let emojiExpandedStatusContent: EmojiStatusComponent.Content let emojiExpandedStatusContent: EmojiStatusComponent.Content
@ -823,6 +824,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
case .premium: case .premium:
emojiRegularStatusContent = .premium(color: navigationContentsAccentColor) emojiRegularStatusContent = .premium(color: navigationContentsAccentColor)
emojiExpandedStatusContent = .premium(color: navigationContentsAccentColor) emojiExpandedStatusContent = .premium(color: navigationContentsAccentColor)
emojiStatusSize = CGSize(width: 30.0, height: 30.0)
case .verified: case .verified:
emojiRegularStatusContent = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .large) emojiRegularStatusContent = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .large)
emojiExpandedStatusContent = .verified(fillColor: navigationContentsAccentColor, foregroundColor: .clear, sizeType: .large) emojiExpandedStatusContent = .verified(fillColor: navigationContentsAccentColor, foregroundColor: .clear, sizeType: .large)
@ -845,6 +847,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
content: emojiRegularStatusContent, content: emojiRegularStatusContent,
size: emojiStatusSize,
isVisibleForAnimations: true, isVisibleForAnimations: true,
useSharedAnimation: true, useSharedAnimation: true,
action: { [weak self] in action: { [weak self] in
@ -866,6 +869,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
animationCache: self.animationCache, animationCache: self.animationCache,
animationRenderer: self.animationRenderer, animationRenderer: self.animationRenderer,
content: emojiExpandedStatusContent, content: emojiExpandedStatusContent,
size: emojiStatusSize,
isVisibleForAnimations: true, isVisibleForAnimations: true,
useSharedAnimation: true, useSharedAnimation: true,
action: { [weak self] in action: { [weak self] in

View File

@ -1173,7 +1173,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
tabsAlpha = 1.0 - tabsOffset / tabsHeight tabsAlpha = 1.0 - tabsOffset / tabsHeight
} }
tabsAlpha *= tabsAlpha tabsAlpha *= tabsAlpha
transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -tabsOffset), size: CGSize(width: size.width, height: tabsHeight))) transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: sideInset, y: -tabsOffset), size: CGSize(width: size.width - sideInset * 2.0, height: tabsHeight)))
transition.updateAlpha(node: self.tabsContainerNode, alpha: tabsAlpha) transition.updateAlpha(node: self.tabsContainerNode, alpha: tabsAlpha)
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel)))
@ -1183,7 +1183,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel)))
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in self.tabsContainerNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
let title: String let title: String
var icons: [TelegramMediaFile] = [] var icons: [TelegramMediaFile] = []
switch key { switch key {

View File

@ -8906,7 +8906,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
private func editingOpenNameColorSetup() { private func editingOpenNameColorSetup() {
if self.peerId == self.context.account.peerId { if self.peerId == self.context.account.peerId {
let controller = PeerNameColorScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, subject: .account) let controller = UserAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData)
self.controller?.push(controller) self.controller?.push(controller)
} else if let peer = self.data?.peer, peer is TelegramChannel { } else if let peer = self.data?.peer, peer is TelegramChannel {
self.controller?.push(ChannelAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, boostStatus: self.boostStatus)) self.controller?.push(ChannelAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, boostStatus: self.boostStatus))
@ -9815,17 +9815,24 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let premiumGiftOptions = self.data?.premiumGiftOptions ?? [] let premiumGiftOptions = self.data?.premiumGiftOptions ?? []
let premiumOptions = premiumGiftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } let premiumOptions = premiumGiftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
var hasBirthday = false
if let cachedUserData = self.data?.cachedData as? CachedUserData {
hasBirthday = hasBirthdayToday(cachedData: cachedUserData)
}
let controller = self.context.sharedContext.makeGiftOptionsController( let controller = self.context.sharedContext.makeGiftOptionsController(
context: self.context, context: self.context,
peerId: self.peerId, peerId: self.peerId,
premiumOptions: premiumOptions, premiumOptions: premiumOptions,
hasBirthday: false, hasBirthday: hasBirthday,
completion: { [weak self] in completion: { [weak self] in
guard let self, let profileGiftsContext = self.data?.profileGiftsContext else { guard let self, let profileGiftsContext = self.data?.profileGiftsContext else {
return return
} }
Queue.mainQueue().after(0.5) {
profileGiftsContext.reload() profileGiftsContext.reload()
} }
}
) )
self.controller?.push(controller) self.controller?.push(controller)
} }
@ -10916,7 +10923,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let controller = self.controller else { guard let controller = self.controller else {
return return
} }
guard let data = self.data, let channel = data.peer as? TelegramChannel, channel.hasPermission(.sendSomething), let giftsContext = data.profileGiftsContext else { guard let data = self.data, let channel = data.peer as? TelegramChannel, let giftsContext = data.profileGiftsContext else {
return return
} }
@ -10979,6 +10986,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
toggleFilter(.unique) toggleFilter(.unique)
}))) })))
if channel.hasPermission(.sendSomething) {
items.append(.separator) items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Displayed, icon: { theme in items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Displayed, icon: { theme in
@ -10991,6 +10999,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, action: { _, f in }, action: { _, f in
toggleFilter(.hidden) toggleFilter(.hidden)
}))) })))
}
return ContextController.Items(content: .list(items)) return ContextController.Items(content: .list(items))
} }
@ -12019,7 +12028,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true)) rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true))
} }
case .gifts: case .gifts:
if let data = self.data, let channel = data.peer as? TelegramChannel, channel.hasPermission(.sendSomething) { if let data = self.data, let channel = data.peer as? TelegramChannel, case .broadcast = channel.info {
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .sort, isForExpandedView: true)) rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .sort, isForExpandedView: true))
} }
default: default:
@ -12295,13 +12304,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.didPlayBirthdayAnimation = true self.didPlayBirthdayAnimation = true
var hasBirthdayToday = false if hasBirthdayToday(cachedData: cachedData) {
let today = Calendar.current.dateComponents(Set([.day, .month]), from: Date())
if today.day == Int(birthday.day) && today.month == Int(birthday.month) {
hasBirthdayToday = true
}
if hasBirthdayToday {
Queue.mainQueue().after(0.3) { Queue.mainQueue().after(0.3) {
var birthdayItemFrame: CGRect? var birthdayItemFrame: CGRect?
if let section = self.regularSections[InfoSection.peerInfo] { if let section = self.regularSections[InfoSection.peerInfo] {

View File

@ -9,6 +9,7 @@ import AccountContext
import ContextUI import ContextUI
import PhotoResources import PhotoResources
import TelegramUIPreferences import TelegramUIPreferences
import TelegramStringFormatting
import ItemListPeerItem import ItemListPeerItem
import ItemListPeerActionItem import ItemListPeerActionItem
import MergeLists import MergeLists
@ -48,6 +49,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private var panelButton: SolidRoundedButtonNode? private var panelButton: SolidRoundedButtonNode?
private var panelCheck: ComponentView<Empty>? private var panelCheck: ComponentView<Empty>?
private let emptyResultsClippingView = UIView()
private let emptyResultsAnimation = ComponentView<Empty>() private let emptyResultsAnimation = ComponentView<Empty>()
private let emptyResultsTitle = ComponentView<Empty>() private let emptyResultsTitle = ComponentView<Empty>()
private let emptyResultsAction = ComponentView<Empty>() private let emptyResultsAction = ComponentView<Empty>()
@ -125,6 +127,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.scrollNode.view.contentInsetAdjustmentBehavior = .never self.scrollNode.view.contentInsetAdjustmentBehavior = .never
self.scrollNode.view.delegate = self self.scrollNode.view.delegate = self
self.emptyResultsClippingView.clipsToBounds = true
self.scrollNode.view.addSubview(self.emptyResultsClippingView)
} }
public func ensureMessageIsVisible(id: MessageId) { public func ensureMessageIsVisible(id: MessageId) {
@ -145,7 +150,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let optionSpacing: CGFloat = 10.0 let optionSpacing: CGFloat = 10.0
let itemsSideInset = params.sideInset + 16.0 let itemsSideInset = params.sideInset + 16.0
let defaultItemsInRow = 3 let defaultItemsInRow = params.size.width > params.size.height ? 5 : 3
let itemsInRow = max(1, min(starsProducts.count, defaultItemsInRow)) let itemsInRow = max(1, min(starsProducts.count, defaultItemsInRow))
let defaultOptionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(defaultItemsInRow - 1)) / CGFloat(defaultItemsInRow) let defaultOptionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(defaultItemsInRow - 1)) / CGFloat(defaultItemsInRow)
let optionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) let optionWidth = (params.size.width - itemsSideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow)
@ -489,6 +494,11 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let emptyAnimationSpacing: CGFloat = 20.0 let emptyAnimationSpacing: CGFloat = 20.0
let emptyTextSpacing: CGFloat = 18.0 let emptyTextSpacing: CGFloat = 18.0
self.emptyResultsClippingView.isHidden = false
transition.setFrame(view: self.emptyResultsClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: self.scrollNode.frame.size))
transition.setBounds(view: self.emptyResultsClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: self.scrollNode.frame.size))
let emptyResultsTitleSize = self.emptyResultsTitle.update( let emptyResultsTitleSize = self.emptyResultsTitle.update(
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
@ -517,7 +527,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return return
} }
self.profileGifts.updateFilter(.All) self.profileGifts.updateFilter(.All)
} },
animateScale: false
) )
), ),
environment: {}, environment: {},
@ -545,7 +556,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if view.superview == nil { if view.superview == nil {
view.alpha = 0.0 view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0) fadeTransition.setAlpha(view: view, alpha: 1.0)
self.scrollNode.view.addSubview(view) self.emptyResultsClippingView.addSubview(view)
view.playOnce() view.playOnce()
} }
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
@ -555,7 +566,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if view.superview == nil { if view.superview == nil {
view.alpha = 0.0 view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0) fadeTransition.setAlpha(view: view, alpha: 1.0)
self.scrollNode.view.addSubview(view) self.emptyResultsClippingView.addSubview(view)
} }
view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
transition.setPosition(view: view, position: emptyResultsTitleFrame.center) transition.setPosition(view: view, position: emptyResultsTitleFrame.center)
@ -564,7 +575,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if view.superview == nil { if view.superview == nil {
view.alpha = 0.0 view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0) fadeTransition.setAlpha(view: view, alpha: 1.0)
self.scrollNode.view.addSubview(view) self.emptyResultsClippingView.addSubview(view)
} }
view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size)
transition.setPosition(view: view, position: emptyResultsActionFrame.center) transition.setPosition(view: view, position: emptyResultsActionFrame.center)
@ -572,6 +583,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} else { } else {
if let view = self.emptyResultsAnimation.view { if let view = self.emptyResultsAnimation.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
self.emptyResultsClippingView.isHidden = true
view.removeFromSuperview() view.removeFromSuperview()
}) })
} }
@ -650,8 +662,21 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.chatControllerInteraction.navigationController()?.pushViewController(controller) self.chatControllerInteraction.navigationController()?.pushViewController(controller)
}) })
} else { } else {
let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: self.peerId, premiumOptions: [], hasBirthday: false, completion: nil) let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Birthday(id: self.peerId))
|> deliverOnMainQueue).start(next: { birthday in
var hasBirthday = false
if let birthday {
hasBirthday = hasBirthdayToday(birthday: birthday)
}
let controller = self.context.sharedContext.makeGiftOptionsController(
context: self.context,
peerId: self.peerId,
premiumOptions: [],
hasBirthday: hasBirthday,
completion: nil
)
self.chatControllerInteraction.navigationController()?.pushViewController(controller) self.chatControllerInteraction.navigationController()?.pushViewController(controller)
})
} }
} }

View File

@ -52,6 +52,8 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl", "//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem", "//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/TelegramUI/Components/EmojiActionIconComponent", "//submodules/TelegramUI/Components/EmojiActionIconComponent",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -1071,7 +1071,8 @@ final class ChannelAppearanceScreenComponent: Component {
isGroup ? environment.strings.Conversation_StatusMembers(Int32($0)) : environment.strings.Conversation_StatusSubscribers(Int32($0)) isGroup ? environment.strings.Conversation_StatusMembers(Int32($0)) : environment.strings.Conversation_StatusSubscribers(Int32($0))
}, },
files: self.cachedIconFiles, files: self.cachedIconFiles,
nameDisplayOrder: presentationData.nameDisplayOrder nameDisplayOrder: presentationData.nameDisplayOrder,
showBackground: false
), ),
params: ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) params: ListViewItemLayoutParams(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
))), ))),

View File

@ -0,0 +1,198 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import GiftItemComponent
import PlainButtonComponent
import TelegramPresentationData
import AccountContext
final class GiftListItemComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let gifts: [StarGift.UniqueGift]
let selectedId: Int64?
let selectionUpdated: (StarGift.UniqueGift) -> Void
let tag: AnyObject?
init(
context: AccountContext,
theme: PresentationTheme,
gifts: [StarGift.UniqueGift],
selectedId: Int64?,
selectionUpdated: @escaping (StarGift.UniqueGift) -> Void,
tag: AnyObject?
) {
self.context = context
self.theme = theme
self.gifts = gifts
self.selectedId = selectedId
self.selectionUpdated = selectionUpdated
self.tag = tag
}
static func ==(lhs: GiftListItemComponent, rhs: GiftListItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.gifts != rhs.gifts {
return false
}
if lhs.selectedId != rhs.selectedId {
return false
}
if lhs.tag !== rhs.tag {
return false
}
return true
}
final class View: UIView, ComponentTaggedView {
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
private var giftItems: [AnyHashable: ComponentView<Empty>] = [:]
private var component: GiftListItemComponent?
private var state: EmptyComponentState?
override public init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var visibleBounds: CGRect?
func updateVisibleBounds(_ bounds: CGRect) {
self.visibleBounds = bounds
self.state?.updated()
}
func update(component: GiftListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let sideInset: CGFloat = 16.0
let topInset: CGFloat = 13.0
let spacing: CGFloat = 10.0
let itemsInRow = 3
let rowsCount = Int(ceil(CGFloat(component.gifts.count) / CGFloat(itemsInRow)))
let itemWidth = floorToScreenPixels((availableSize.width - sideInset * 2.0 - spacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow))
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: itemWidth, height: itemWidth))
let contentHeight = topInset * 2.0 + itemWidth * CGFloat(rowsCount) + spacing * CGFloat(rowsCount - 1)
var index: Int32 = 0
for gift in component.gifts {
var isVisible = false
if let visibleBounds = self.visibleBounds, visibleBounds.intersects(itemFrame) {
isVisible = true
}
if isVisible {
let id = gift.id
let itemId = AnyHashable(id)
validIds.append(itemId)
var itemTransition = transition
let visibleItem: ComponentView<Empty>
if let current = self.giftItems[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
self.giftItems[itemId] = visibleItem
itemTransition = .immediate
}
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
GiftItemComponent(
context: component.context,
theme: component.theme,
peer: nil,
subject: .uniqueGift(gift: gift),
ribbon: nil,
isHidden: false,
isSelected: gift.id == component.selectedId,
mode: .grid
)
),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.selectionUpdated(gift)
},
animateAlpha: false
)
),
environment: {},
containerSize: itemFrame.size
)
if let itemView = visibleItem.view {
if itemView.superview == nil {
self.addSubview(itemView)
if !transition.animation.isImmediate {
itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
}
itemFrame.origin.x += itemFrame.width + spacing
if itemFrame.maxX > availableSize.width {
itemFrame.origin.x = sideInset
itemFrame.origin.y += itemFrame.height + spacing
}
index += 1
}
var removeIds: [AnyHashable] = []
for (id, item) in self.giftItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemView = item.view {
if !transition.animation.isImmediate {
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
itemView.removeFromSuperview()
})
} else {
itemView.removeFromSuperview()
}
}
}
}
for id in removeIds {
self.giftItems.removeValue(forKey: id)
}
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -308,7 +308,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
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, delay: 0.25, removeOnCompletion: false, completion: { _ in snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, removeOnCompletion: false, completion: { _ in
snapshot.removeFromSuperview() snapshot.removeFromSuperview()
}) })
} }

View File

@ -29,8 +29,9 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
let subtitleString: String? let subtitleString: String?
let files: [Int64: TelegramMediaFile] let files: [Int64: TelegramMediaFile]
let nameDisplayOrder: PresentationPersonNameOrder let nameDisplayOrder: PresentationPersonNameOrder
let showBackground: Bool
init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, topInset: CGFloat, sectionId: ItemListSectionId, peer: EnginePeer?, subtitleString: String? = nil, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder) { init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, topInset: CGFloat, sectionId: ItemListSectionId, peer: EnginePeer?, subtitleString: String? = nil, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder, showBackground: Bool) {
self.context = context self.context = context
self.theme = theme self.theme = theme
self.componentTheme = componentTheme self.componentTheme = componentTheme
@ -41,6 +42,7 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
self.subtitleString = subtitleString self.subtitleString = subtitleString
self.files = files self.files = files
self.nameDisplayOrder = nameDisplayOrder self.nameDisplayOrder = nameDisplayOrder
self.showBackground = showBackground
} }
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) { 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) {
@ -102,7 +104,9 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
if lhs.nameDisplayOrder != rhs.nameDisplayOrder { if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false return false
} }
if lhs.showBackground != rhs.showBackground {
return false
}
return true return true
} }
} }
@ -114,6 +118,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
private let subtitle = ComponentView<Empty>() private let subtitle = ComponentView<Empty>()
private var icon: ComponentView<Empty>? private var icon: ComponentView<Empty>?
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode private let maskNode: ASImageNode
@ -121,6 +126,9 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
private var item: PeerNameColorProfilePreviewItem? private var item: PeerNameColorProfilePreviewItem?
init() { init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode() self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true self.topStripeNode.isLayerBacked = true
@ -138,9 +146,6 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
self.isUserInteractionEnabled = false self.isUserInteractionEnabled = false
} }
deinit {
}
func asyncLayout() -> (_ item: PeerNameColorProfilePreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { func asyncLayout() -> (_ item: PeerNameColorProfilePreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
return { [weak self] item, params, neighbors in return { [weak self] item, params, neighbors in
let separatorHeight = UIScreenPixel let separatorHeight = UIScreenPixel
@ -158,15 +163,19 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
guard let self else { guard let self else {
return return
} }
if let previousItem = self.item, (previousItem.peer?.profileColor != item.peer?.profileColor) || (previousItem.peer?.profileBackgroundEmojiId != item.peer?.profileBackgroundEmojiId) { if let previousItem = self.item, (previousItem.peer?.nameColor != item.peer?.nameColor) || (previousItem.peer?.profileColor != item.peer?.profileColor) || (previousItem.peer?.profileBackgroundEmojiId != item.peer?.profileBackgroundEmojiId) {
UIView.transition(with: self.view, duration: 0.2, options: UIView.AnimationOptions.transitionCrossDissolve, animations: { UIView.transition(with: self.view, duration: 0.2, options: UIView.AnimationOptions.transitionCrossDissolve, animations: {
}) })
} }
self.item = item self.item = item
self.backgroundNode.backgroundColor = item.theme.rootController.navigationBar.opaqueBackgroundColor
self.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor self.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
self.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor self.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if self.backgroundNode.supernode == nil {
self.addSubnode(self.backgroundNode)
}
if self.topStripeNode.supernode == nil { if self.topStripeNode.supernode == nil {
self.addSubnode(self.topStripeNode) self.addSubnode(self.topStripeNode)
} }
@ -178,10 +187,19 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
} }
if params.isStandalone { if params.isStandalone {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.backgroundNode, alpha: item.showBackground ? 1.0 : 0.0)
transition.updateAlpha(node: self.bottomStripeNode, alpha: item.showBackground ? 1.0 : 0.0)
self.backgroundNode.isHidden = false
self.topStripeNode.isHidden = true self.topStripeNode.isHidden = true
self.bottomStripeNode.isHidden = true self.bottomStripeNode.isHidden = false
self.maskNode.isHidden = true self.maskNode.isHidden = true
self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width, height: separatorHeight))
} else { } else {
self.backgroundNode.isHidden = true
let hasCorners = itemListHasRoundedBlockLayout(params) let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false var hasTopCorners = false
var hasBottomCorners = false var hasBottomCorners = false
@ -219,11 +237,19 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
let avatarSize: CGFloat = 104.0 let avatarSize: CGFloat = 104.0
let avatarFrame = CGRect(origin: CGPoint(x: floor((coverFrame.width - avatarSize) * 0.5), y: coverFrame.minY + item.topInset + 24.0), size: CGSize(width: avatarSize, height: avatarSize)) let avatarFrame = CGRect(origin: CGPoint(x: floor((coverFrame.width - avatarSize) * 0.5), y: coverFrame.minY + item.topInset + 24.0), size: CGSize(width: avatarSize, height: avatarSize))
let subject: PeerInfoCoverComponent.Subject?
if let status = item.peer?.emojiStatus, case .starGift = status.content {
subject = .status(status)
} else if let peer = item.peer {
subject = .peer(peer)
} else {
subject = nil
}
let _ = self.background.update( let _ = self.background.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(PeerInfoCoverComponent( component: AnyComponent(PeerInfoCoverComponent(
context: item.context, context: item.context,
subject: item.peer.flatMap { .peer($0) }, subject: subject,
files: item.files, files: item.files,
isDark: item.theme.overallDarkAppearance, isDark: item.theme.overallDarkAppearance,
avatarCenter: avatarFrame.center, avatarCenter: avatarFrame.center,
@ -238,7 +264,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
if let backgroundView = self.background.view { if let backgroundView = self.background.view {
if backgroundView.superview == nil { if backgroundView.superview == nil {
backgroundView.clipsToBounds = true backgroundView.clipsToBounds = true
self.view.insertSubview(backgroundView, at: 0) self.view.insertSubview(backgroundView, at: 1)
} }
backgroundView.frame = coverFrame backgroundView.frame = coverFrame
} }
@ -296,7 +322,9 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
} }
let statusColor: UIColor let statusColor: UIColor
if let peer = item.peer, peer.profileColor != nil { if let status = item.peer?.emojiStatus, case .starGift = status.content {
statusColor = .white
} else if let peer = item.peer, peer.profileColor != nil {
statusColor = .white statusColor = .white
} else { } else {
statusColor = item.theme.list.itemCheckColors.fillColor statusColor = item.theme.list.itemCheckColors.fillColor
@ -321,7 +349,13 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
let backgroundColor: UIColor let backgroundColor: UIColor
let titleColor: UIColor let titleColor: UIColor
let subtitleColor: UIColor let subtitleColor: UIColor
if let peer = item.peer, let profileColor = peer.profileColor { var particleColor: UIColor?
if let status = item.peer?.emojiStatus, case let .starGift(_, _, _, _, _, _, outerColor, _, _) = status.content {
titleColor = .white
backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColor))
subtitleColor = UIColor(white: 1.0, alpha: 0.6).blitOver(backgroundColor.withMultiplied(hue: 1.0, saturation: 2.2, brightness: 1.5), alpha: 1.0)
particleColor = .white
} else if let peer = item.peer, let profileColor = peer.profileColor {
titleColor = .white titleColor = .white
backgroundColor = item.context.peerNameColors.getProfile(profileColor).main backgroundColor = item.context.peerNameColors.getProfile(profileColor).main
subtitleColor = UIColor(white: 1.0, alpha: 0.6).blitOver(backgroundColor.withMultiplied(hue: 1.0, saturation: 2.2, brightness: 1.5), alpha: 1.0) subtitleColor = UIColor(white: 1.0, alpha: 0.6).blitOver(backgroundColor.withMultiplied(hue: 1.0, saturation: 2.2, brightness: 1.5), alpha: 1.0)
@ -384,6 +418,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
animationCache: item.context.animationCache, animationCache: item.context.animationCache,
animationRenderer: item.context.animationRenderer, animationRenderer: item.context.animationRenderer,
content: emojiStatusContent, content: emojiStatusContent,
particleColor: particleColor,
isVisibleForAnimations: true, isVisibleForAnimations: true,
action: nil action: nil
)), )),
@ -424,6 +459,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
} }
self.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0) self.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0)
self.backgroundNode.frame = backgroundFrame
}) })
} }
} }

View File

@ -1,936 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import UndoUI
import EntityKeyboard
import PremiumUI
import PeerNameColorItem
private final class PeerNameColorScreenArguments {
let context: AccountContext
let updateNameColor: (PeerNameColor?) -> Void
let updateBackgroundEmojiId: (Int64?, TelegramMediaFile?) -> Void
let resetColor: () -> Void
init(
context: AccountContext,
updateNameColor: @escaping (PeerNameColor?) -> Void,
updateBackgroundEmojiId: @escaping (Int64?, TelegramMediaFile?) -> Void,
resetColor: @escaping () -> Void
) {
self.context = context
self.updateNameColor = updateNameColor
self.updateBackgroundEmojiId = updateBackgroundEmojiId
self.resetColor = resetColor
}
}
private enum PeerNameColorScreenSection: Int32 {
case nameColor
case backgroundEmoji
}
private enum PeerNameColorScreenEntry: ItemListNodeEntry {
enum StableId: Hashable {
case colorHeader
case colorMessage
case colorProfile
case colorPicker
case removeColor
case colorDescription
case backgroundEmojiHeader
case backgroundEmoji
}
case colorHeader(String)
case colorMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, items: [PeerNameColorChatPreviewItem.MessageItem])
case colorProfile(peer: EnginePeer?, files: [Int64: TelegramMediaFile], nameDisplayOrder: PresentationPersonNameOrder)
case colorPicker(colors: PeerNameColors, currentColor: PeerNameColor?, isProfile: Bool)
case removeColor
case colorDescription(String)
case backgroundEmojiHeader(String, String?)
case backgroundEmoji(EmojiPagerContentComponent, UIColor, Bool, Bool)
var section: ItemListSectionId {
switch self {
case .colorHeader, .colorMessage, .colorProfile, .colorPicker, .removeColor, .colorDescription:
return PeerNameColorScreenSection.nameColor.rawValue
case .backgroundEmojiHeader, .backgroundEmoji:
return PeerNameColorScreenSection.backgroundEmoji.rawValue
}
}
var stableId: StableId {
switch self {
case .colorHeader:
return .colorHeader
case .colorMessage:
return .colorMessage
case .colorProfile:
return .colorProfile
case .colorPicker:
return .colorPicker
case .removeColor:
return.removeColor
case .colorDescription:
return .colorDescription
case .backgroundEmojiHeader:
return .backgroundEmojiHeader
case .backgroundEmoji:
return .backgroundEmoji
}
}
var sortId: Int {
switch self {
case .colorHeader:
return 0
case .colorMessage:
return 1
case .colorProfile:
return 2
case .colorPicker:
return 3
case .removeColor:
return 4
case .colorDescription:
return 5
case .backgroundEmojiHeader:
return 6
case .backgroundEmoji:
return 7
}
}
static func ==(lhs: PeerNameColorScreenEntry, rhs: PeerNameColorScreenEntry) -> Bool {
switch lhs {
case let .colorHeader(text):
if case .colorHeader(text) = rhs {
return true
} else {
return false
}
case let .colorMessage(lhsWallpaper, lhsFontSize, lhsBubbleCorners, lhsDateTimeFormat, lhsNameDisplayOrder, lhsItems):
if case let .colorMessage(rhsWallpaper, rhsFontSize, rhsBubbleCorners, rhsDateTimeFormat, rhsNameDisplayOrder, rhsItems) = rhs, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsBubbleCorners == rhsBubbleCorners, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameDisplayOrder == rhsNameDisplayOrder, lhsItems == rhsItems {
return true
} else {
return false
}
case let .colorProfile(lhsPeer, lhsFiles, lhsNameDisplayOrder):
if case let .colorProfile(rhsPeer, rhsFiles, rhsNameDisplayOrder) = rhs {
if lhsPeer != rhsPeer {
return false
}
if lhsFiles != rhsFiles {
return false
}
if lhsNameDisplayOrder != rhsNameDisplayOrder {
return false
}
return true
} else {
return false
}
case let .colorPicker(lhsColors, lhsCurrentColor, lhsIsProfile):
if case let .colorPicker(rhsColors, rhsCurrentColor, rhsIsProfile) = rhs, lhsColors == rhsColors, lhsCurrentColor == rhsCurrentColor, lhsIsProfile == rhsIsProfile {
return true
} else {
return false
}
case .removeColor:
if case .removeColor = rhs {
return true
} else {
return false
}
case let .colorDescription(text):
if case .colorDescription(text) = rhs {
return true
} else {
return false
}
case let .backgroundEmojiHeader(text, action):
if case .backgroundEmojiHeader(text, action) = rhs {
return true
} else {
return false
}
case let .backgroundEmoji(lhsEmojiContent, lhsBackgroundIconColor, lhsIsProfile, lhsHasRemoveButton):
if case let .backgroundEmoji(rhsEmojiContent, rhsBackgroundIconColor, rhsIsProfile, rhsHasRemoveButton) = rhs, lhsEmojiContent == rhsEmojiContent, lhsBackgroundIconColor == rhsBackgroundIconColor, lhsIsProfile == rhsIsProfile, lhsHasRemoveButton == rhsHasRemoveButton {
return true
} else {
return false
}
}
}
static func <(lhs: PeerNameColorScreenEntry, rhs: PeerNameColorScreenEntry) -> Bool {
return lhs.sortId < rhs.sortId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! PeerNameColorScreenArguments
switch self {
case let .colorHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .colorMessage(wallpaper, fontSize, chatBubbleCorners, dateTimeFormat, nameDisplayOrder, items):
return PeerNameColorChatPreviewItem(
context: arguments.context,
theme: presentationData.theme,
componentTheme: presentationData.theme,
strings: presentationData.strings,
sectionId: self.section,
fontSize: fontSize,
chatBubbleCorners: chatBubbleCorners,
wallpaper: wallpaper,
dateTimeFormat: dateTimeFormat,
nameDisplayOrder: nameDisplayOrder,
messageItems: items
)
case let .colorProfile(peer, files, nameDisplayOrder):
return PeerNameColorProfilePreviewItem(
context: arguments.context,
theme: presentationData.theme,
componentTheme: presentationData.theme,
strings: presentationData.strings,
topInset: 0.0,
sectionId: self.section,
peer: peer,
files: files,
nameDisplayOrder: nameDisplayOrder
)
case let .colorPicker(colors, currentColor, isProfile):
return PeerNameColorItem(
theme: presentationData.theme,
colors: colors,
mode: isProfile ? .profile : .name,
currentColor: currentColor,
updated: { color in
if let color {
arguments.updateNameColor(color)
}
},
sectionId: self.section
)
case .removeColor:
return ItemListActionItem(presentationData: presentationData, title: presentationData.strings.ProfileColorSetup_ResetAction, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.resetColor()
})
case let .colorDescription(text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .backgroundEmojiHeader(text, action):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, actionText: action, action: action != nil ? {
arguments.updateBackgroundEmojiId(0, nil)
} : nil, sectionId: self.section)
case let .backgroundEmoji(emojiContent, backgroundIconColor, isProfileColor, hasRemoveButton):
return EmojiPickerItem(context: arguments.context, theme: presentationData.theme, strings: presentationData.strings, emojiContent: emojiContent, backgroundIconColor: backgroundIconColor, isProfileColor: isProfileColor, hasRemoveButton: hasRemoveButton, sectionId: self.section)
}
}
}
private struct PeerNameColorScreenState: Equatable {
var updatedNameColor: PeerNameColor?
var updatedBackgroundEmojiId: Int64?
var inProgress: Bool = false
var needsBoosts: Bool = false
var updatedProfileColor: PeerNameColor?
var hasUpdatedProfileColor: Bool = false
var updatedProfileBackgroundEmojiId: Int64?
var hasUpdatedProfileBackgroundEmojiId: Bool = false
var selectedTabIndex: Int = 0
var files: [Int64: TelegramMediaFile] = [:]
}
private func peerNameColorScreenEntries(
nameColors: PeerNameColors,
presentationData: PresentationData,
state: PeerNameColorScreenState,
peer: EnginePeer?,
isPremium: Bool,
emojiContent: EmojiPagerContentComponent?
) -> [PeerNameColorScreenEntry] {
var entries: [PeerNameColorScreenEntry] = []
if let peer {
let nameColor: PeerNameColor
if let updatedNameColor = state.updatedNameColor {
nameColor = updatedNameColor
} else if let peerNameColor = peer.nameColor {
nameColor = peerNameColor
} else {
nameColor = .blue
}
let colors = nameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance)
let backgroundEmojiId: Int64?
if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId {
if updatedBackgroundEmojiId == 0 {
backgroundEmojiId = nil
} else {
backgroundEmojiId = updatedBackgroundEmojiId
}
} else if let emojiId = peer.backgroundEmojiId {
backgroundEmojiId = emojiId
} else {
backgroundEmojiId = nil
}
let profileColor: PeerNameColor?
if state.hasUpdatedProfileColor {
profileColor = state.updatedProfileColor
} else {
profileColor = peer.profileColor
}
var selectedProfileEmojiId: Int64?
if state.hasUpdatedProfileBackgroundEmojiId {
selectedProfileEmojiId = state.updatedProfileBackgroundEmojiId
} else {
selectedProfileEmojiId = peer.profileBackgroundEmojiId
}
let profileColors = profileColor.flatMap { profileColor in nameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance) }
let replyText: String
let messageText: String
if case .channel = peer {
replyText = presentationData.strings.NameColor_ChatPreview_ReplyText_Channel
messageText = presentationData.strings.NameColor_ChatPreview_MessageText_Channel
} else {
replyText = presentationData.strings.NameColor_ChatPreview_ReplyText_Account
messageText = presentationData.strings.NameColor_ChatPreview_MessageText_Account
}
let messageItem = PeerNameColorChatPreviewItem.MessageItem(
outgoing: false,
peerId: PeerId(namespace: peer.id.namespace, id: PeerId.Id._internalFromInt64Value(0)),
author: peer.compactDisplayTitle,
photo: peer.profileImageRepresentations,
nameColor: nameColor,
backgroundEmojiId: backgroundEmojiId,
reply: (peer.compactDisplayTitle, replyText, nameColor),
linkPreview: (presentationData.strings.NameColor_ChatPreview_LinkSite, presentationData.strings.NameColor_ChatPreview_LinkTitle, presentationData.strings.NameColor_ChatPreview_LinkText),
text: messageText
)
if state.selectedTabIndex == 0 {
entries.append(.colorMessage(
wallpaper: presentationData.chatWallpaper,
fontSize: presentationData.chatFontSize,
bubbleCorners: presentationData.chatBubbleCorners,
dateTimeFormat: presentationData.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
items: [messageItem]
))
} else {
var updatedPeer = peer
switch updatedPeer {
case let .user(user):
updatedPeer = .user(user.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId).withUpdatedProfileColor(profileColor).withUpdatedProfileBackgroundEmojiId(selectedProfileEmojiId))
case let .channel(channel):
updatedPeer = .channel(channel.withUpdatedNameColor(nameColor).withUpdatedBackgroundEmojiId(backgroundEmojiId).withUpdatedProfileColor(profileColor).withUpdatedProfileBackgroundEmojiId(selectedProfileEmojiId))
default:
break
}
var files: [Int64: TelegramMediaFile] = [:]
if let fileId = updatedPeer.profileBackgroundEmojiId, let file = state.files[fileId] {
files[fileId] = file
}
entries.append(.colorProfile(
peer: updatedPeer,
files: files,
nameDisplayOrder: presentationData.nameDisplayOrder
))
}
if state.selectedTabIndex == 0 {
entries.append(.colorPicker(
colors: nameColors,
currentColor: nameColor,
isProfile: false
))
} else {
entries.append(.colorPicker(
colors: nameColors,
currentColor: profileColor,
isProfile: true
))
}
if state.selectedTabIndex == 1 && profileColor != nil {
entries.append(.removeColor)
}
if state.selectedTabIndex == 0 {
if case .channel = peer {
entries.append(.colorDescription(presentationData.strings.NameColor_ChatPreview_Description_Channel))
} else {
entries.append(.colorDescription(presentationData.strings.NameColor_ChatPreview_Description_Account))
}
if let emojiContent {
var selectedItems = Set<MediaId>()
if let backgroundEmojiId {
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: backgroundEmojiId))
}
let emojiContent = emojiContent.withSelectedItems(selectedItems).withCustomTintColor(colors.main)
entries.append(.backgroundEmojiHeader(presentationData.strings.NameColor_BackgroundEmoji_Title, (backgroundEmojiId != nil && backgroundEmojiId != 0) ? presentationData.strings.NameColor_BackgroundEmoji_Remove : nil))
entries.append(.backgroundEmoji(emojiContent, colors.main, false, false))
}
} else {
if let emojiContent {
var selectedItems = Set<MediaId>()
if let selectedProfileEmojiId {
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: selectedProfileEmojiId))
}
let emojiContent = emojiContent.withSelectedItems(selectedItems).withCustomTintColor(profileColors?.main ?? presentationData.theme.list.itemSecondaryTextColor)
entries.append(.backgroundEmojiHeader(presentationData.strings.ProfileColorSetup_IconSectionTitle, (selectedProfileEmojiId != nil && selectedProfileEmojiId != 0) ? presentationData.strings.NameColor_BackgroundEmoji_Remove : nil))
entries.append(.backgroundEmoji(emojiContent, profileColors?.main ?? presentationData.theme.list.itemSecondaryTextColor, true, profileColor != nil))
} else {
if case .channel = peer {
entries.append(.colorDescription(presentationData.strings.ProfileColorSetup_ChannelColorInfoLabel))
} else {
entries.append(.colorDescription(presentationData.strings.ProfileColorSetup_AccountColorInfoLabel))
}
}
}
}
return entries
}
public enum PeerNameColorScreenSubject {
case account
case channel(EnginePeer.Id)
}
public func PeerNameColorScreen(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
subject: PeerNameColorScreenSubject
) -> ViewController {
let statePromise = ValuePromise(PeerNameColorScreenState(), ignoreRepeated: true)
let stateValue = Atomic(value: PeerNameColorScreenState())
let updateState: ((PeerNameColorScreenState) -> PeerNameColorScreenState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
var presentImpl: ((ViewController) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var attemptNavigationImpl: ((@escaping () -> Void) -> Bool)?
var applyChangesImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let arguments = PeerNameColorScreenArguments(
context: context,
updateNameColor: { color in
updateState { state in
var updatedState = state
if state.selectedTabIndex == 0 {
if let color {
updatedState.updatedNameColor = color
}
} else {
updatedState.updatedProfileColor = color
updatedState.hasUpdatedProfileColor = true
}
return updatedState
}
},
updateBackgroundEmojiId: { emojiId, file in
updateState { state in
var updatedState = state
if state.selectedTabIndex == 0 {
updatedState.updatedBackgroundEmojiId = emojiId
} else {
updatedState.hasUpdatedProfileBackgroundEmojiId = true
updatedState.updatedProfileBackgroundEmojiId = emojiId
}
if let file {
updatedState.files[file.fileId.id] = file
}
return updatedState
}
},
resetColor: {
updateState { state in
var updatedState = state
if state.selectedTabIndex == 1 {
updatedState.updatedProfileColor = nil
updatedState.hasUpdatedProfileColor = true
updatedState.updatedProfileBackgroundEmojiId = nil
updatedState.hasUpdatedProfileBackgroundEmojiId = true
}
return updatedState
}
}
)
let peerId: EnginePeer.Id
switch subject {
case .account:
peerId = context.account.peerId
case let .channel(channelId):
peerId = channelId
}
let emojiContent = EmojiPagerContentComponent.emojiInputData(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
isStandalone: false,
subject: .backgroundIcon,
hasTrending: false,
topReactionItems: [],
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId,
selectedItems: Set(),
backgroundIconColor: nil
)
/*let emojiContent: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.sharedContext.presentationData
)
|> mapToSignal { presentationData, state, peer -> Signal<(EmojiPagerContentComponent, EmojiPagerContentComponent), NoError> in
var selectedEmojiId: Int64?
if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId {
selectedEmojiId = updatedBackgroundEmojiId
} else {
selectedEmojiId = peer?.backgroundEmojiId
}
let nameColor: PeerNameColor
if let updatedNameColor = state.updatedNameColor {
nameColor = updatedNameColor
} else {
nameColor = (peer?.nameColor ?? .blue)
}
var selectedProfileEmojiId: Int64?
if state.hasUpdatedProfileBackgroundEmojiId {
selectedProfileEmojiId = state.updatedProfileBackgroundEmojiId
} else {
selectedProfileEmojiId = peer?.profileBackgroundEmojiId
}
let profileColor: PeerNameColor?
if state.hasUpdatedProfileColor {
profileColor = state.updatedProfileColor
} else {
profileColor = peer?.profileColor
}
let color = context.peerNameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance)
let profileColorValue: UIColor? = profileColor.flatMap { profileColor in context.peerNameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance).main }
let selectedItems: [EngineMedia.Id]
if let selectedEmojiId, selectedEmojiId != 0 {
selectedItems = [EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: selectedEmojiId)]
} else {
selectedItems = []
}
let selectedProfileItems: [EngineMedia.Id]
if let selectedProfileEmojiId, selectedProfileEmojiId != 0 {
selectedProfileItems = [EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: selectedProfileEmojiId)]
} else {
selectedProfileItems = []
}
}*/
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: .mainQueue(),
presentationData,
statePromise.get(),
context.engine.stickers.availableReactions(),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
emojiContent
)
|> deliverOnMainQueue
|> map { presentationData, state, availableReactions, peer, emojiContent -> (ItemListControllerState, (ItemListNodeState, Any)) in
let isPremium = peer?.isPremium ?? false
let buttonTitle: String
let isLocked: Bool
switch subject {
case .account:
isLocked = !isPremium
case .channel:
isLocked = false
}
let _ = isLocked
let backgroundEmojiId: Int64
if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId {
backgroundEmojiId = updatedBackgroundEmojiId
} else if let emojiId = peer?.backgroundEmojiId {
backgroundEmojiId = emojiId
} else {
backgroundEmojiId = 0
}
if backgroundEmojiId != 0 {
buttonTitle = presentationData.strings.NameColor_ApplyColorAndBackgroundEmoji
} else {
buttonTitle = presentationData.strings.NameColor_ApplyColor
}
let _ = buttonTitle
/*let footerItem = ApplyColorFooterItem(
theme: presentationData.theme,
title: buttonTitle,
locked: isLocked,
inProgress: state.inProgress,
action: {
if !isLocked {
applyChangesImpl?()
} else {
HapticFeedback().impact(.light)
let controller = UndoOverlayController(
presentationData: presentationData,
content: .premiumPaywall(
title: nil,
text: presentationData.strings.NameColor_TooltipPremium_Account,
customUndoText: nil,
timeout: nil,
linkAction: nil
),
elevatedLayout: false,
action: { action in
if case .info = action {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .nameColor, forceDark: false, dismissed: nil)
pushImpl?(controller)
}
return true
}
)
presentImpl?(controller)
}
}
)*/
emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { _, item, _, _, _, _ in
var selectedFileId: Int64?
var selectedFile: TelegramMediaFile?
if let fileId = item.itemFile?.fileId.id {
selectedFileId = fileId
selectedFile = item.itemFile
} else {
selectedFileId = 0
}
arguments.updateBackgroundEmojiId(selectedFileId, selectedFile)
},
deleteBackwards: {
},
openStickerSettings: {
},
openFeatured: {
},
openSearch: {
},
addGroupAction: { groupId, isPremiumLocked, _ in
guard let collectionId = groupId.base as? ItemCollectionId else {
return
}
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredEmojiPack.info.id == collectionId {
let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start()
break
}
}
})
},
clearGroup: { _ in
},
editAction: { _ in
},
pushController: { c in
},
presentController: { c in
},
presentGlobalOverlayController: { c in
},
navigationController: {
return nil
},
requestUpdate: { _ in
},
updateSearchQuery: { _ in
},
updateScrollingToItemGroup: {
},
onScroll: {},
chatPeerId: nil,
peekBehavior: nil,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil,
addImage: nil
)
let entries = peerNameColorScreenEntries(
nameColors: context.peerNameColors,
presentationData: presentationData,
state: state,
peer: peer,
isPremium: isPremium,
emojiContent: emojiContent
)
let title: ItemListControllerTitle = .sectionControl([presentationData.strings.ProfileColorSetup_TitleName, presentationData.strings.ProfileColorSetup_TitleProfile], state.selectedTabIndex)
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: title,
leftNavigationButton: nil,
rightNavigationButton: ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
if !isLocked {
applyChangesImpl?()
} else {
HapticFeedback().impact(.light)
let controller = UndoOverlayController(
presentationData: presentationData,
content: .premiumPaywall(
title: nil,
text: presentationData.strings.NameColor_TooltipPremium_Account,
customUndoText: nil,
timeout: nil,
linkAction: nil
),
elevatedLayout: false,
action: { action in
if case .info = action {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .colors, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushImpl?(controller)
}
return true
}
)
presentImpl?(controller)
}
}),
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: false
)
let listState = ItemListNodeState(
presentationData: ItemListPresentationData(presentationData),
entries: entries,
style: .blocks,
footerItem: nil,
animateChanges: false
)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.titleControlValueChanged = { value in
updateState { state in
var state = state
state.selectedTabIndex = value
return state
}
}
presentImpl = { [weak controller] c in
guard let controller else {
return
}
if c is UndoOverlayController {
controller.present(c, in: .current)
} else {
controller.present(c, in: .window(.root))
}
}
pushImpl = { [weak controller] c in
guard let controller else {
return
}
controller.push(c)
}
dismissImpl = { [weak controller] in
guard let controller else {
return
}
controller.dismiss()
}
controller.attemptNavigation = { f in
return attemptNavigationImpl?(f) ?? true
}
attemptNavigationImpl = { f in
if case .account = subject, !context.isPremium {
return true
}
let state = stateValue.with({ $0 })
if case .channel = subject, state.needsBoosts {
return true
}
var hasChanges = false
if state.updatedNameColor != nil || state.updatedBackgroundEmojiId != nil {
hasChanges = true
}
if hasChanges {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.NameColor_UnsavedChanges_Title, text: presentationData.strings.NameColor_UnsavedChanges_Text, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.NameColor_UnsavedChanges_Discard, action: {
f()
dismissImpl?()
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.NameColor_UnsavedChanges_Apply, action: {
applyChangesImpl?()
})
]))
return false
} else {
return true
}
}
applyChangesImpl = { [weak controller] in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let peer else {
return
}
let state = stateValue.with { $0 }
if state.updatedNameColor == nil && state.updatedBackgroundEmojiId == nil && !state.hasUpdatedProfileColor && !state.hasUpdatedProfileBackgroundEmojiId {
dismissImpl?()
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let nameColor = state.updatedNameColor ?? peer.nameColor
let backgroundEmojiId = state.updatedBackgroundEmojiId ?? peer.backgroundEmojiId
let colors = context.peerNameColors.get(nameColor ?? .blue, dark: presentationData.theme.overallDarkAppearance)
let profileColor = state.hasUpdatedProfileColor ? state.updatedProfileColor : peer.profileColor
let profileBackgroundEmojiId = state.hasUpdatedProfileBackgroundEmojiId ? state.updatedProfileBackgroundEmojiId : peer.profileBackgroundEmojiId
switch subject {
case .account:
let _ = context.engine.accountData.updateNameColorAndEmoji(nameColor: nameColor ?? .blue, backgroundEmojiId: backgroundEmojiId ?? 0, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId ?? 0).startStandalone()
if let navigationController = controller?.navigationController as? NavigationController {
Queue.mainQueue().after(0.25) {
if let lastController = navigationController.viewControllers.last as? ViewController {
var colorList: [PeerNameColors.Colors] = []
if let nameColor {
colorList.append(context.peerNameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance))
}
if let profileColor {
colorList.append(context.peerNameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance, subject: .palette))
}
let colorImage = generateSettingsMenuPeerColorsLabelIcon(colors: colorList)
let tipController = UndoOverlayController(presentationData: presentationData, content: .image(image: colorImage, title: nil, text: presentationData.strings.ProfileColorSetup_ToastAccountColorUpdated, round: false, undoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false })
lastController.present(tipController, in: .window(.root))
}
}
}
dismissImpl?()
case let .channel(peerId):
updateState { state in
var updatedState = state
updatedState.inProgress = true
return updatedState
}
let _ = (context.engine.peers.updatePeerNameColorAndEmoji(peerId: peerId, nameColor: nameColor ?? .blue, backgroundEmojiId: backgroundEmojiId ?? 0, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId ?? 0)
|> deliverOnMainQueue).startStandalone(next: {
}, error: { error in
if case .channelBoostRequired = error {
updateState { state in
var updatedState = state
updatedState.needsBoosts = true
return updatedState
}
let _ = combineLatest(
queue: Queue.mainQueue(),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
context.engine.peers.getChannelBoostStatus(peerId: peerId)
).startStandalone(next: { peer, status in
guard let peer, let status else {
return
}
updateState { state in
var updatedState = state
updatedState.inProgress = false
return updatedState
}
let link = status.url
let controller = PremiumLimitScreen(context: context, subject: .storiesChannelBoost(peer: peer, boostSubject: .nameColors(colors: .blue), isCurrent: true, level: Int32(status.level), currentLevelBoosts: Int32(status.currentLevelBoosts), nextLevelBoosts: status.nextLevelBoosts.flatMap(Int32.init), link: link, myBoostCount: 0, canBoostAgain: false), count: Int32(status.boosts), action: {
UIPasteboard.general.string = link
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.ChannelBoost_BoostLinkCopied), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }))
return true
}, openStats: nil, openGift: premiumConfiguration.giveawayGiftsPurchaseAvailable ? {
let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic)
pushImpl?(controller)
} : nil)
pushImpl?(controller)
HapticFeedback().impact(.light)
})
} else {
updateState { state in
var updatedState = state
updatedState.inProgress = false
return updatedState
}
}
}, completed: {
if let navigationController = controller?.navigationController as? NavigationController {
Queue.mainQueue().after(0.25) {
if let lastController = navigationController.viewControllers.last as? ViewController {
let tipController = UndoOverlayController(presentationData: presentationData, content: .image(image: generatePeerNameColorImage(nameColor: colors, isDark: presentationData.theme.overallDarkAppearance, bounds: CGSize(width: 32.0, height: 32.0), size: CGSize(width: 22.0, height: 22.0))!, title: nil, text: presentationData.strings.NameColor_ChannelColorUpdated, round: false, undoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false })
lastController.present(tipController, in: .window(.root))
}
}
}
dismissImpl?()
})
}
})
}
return controller
}

View File

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

View File

@ -4339,7 +4339,22 @@ extension ChatControllerImpl {
let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: self.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: self.context.sharedContext.accountManager, peerId: peerId, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone()
} }
} else { } else {
let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: [], hasBirthday: false, completion: nil) let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: [], hasBirthday: false, completion: { [weak self] in
guard let self, let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
if let controller = self.context.sharedContext.makePeerInfoController(
context: self.context,
updatedPresentationData: nil,
peer: peer,
mode: .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
self.push(controller)
}
})
self.push(controller) self.push(controller)
} }
}, openPremiumRequiredForMessaging: { [weak self] in }, openPremiumRequiredForMessaging: { [weak self] in

View File

@ -4577,7 +4577,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return return
} }
let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
let controller = self.context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: false, completion: nil)
var hasBirthday = false
if let cachedUserData = self.peerView?.cachedData as? CachedUserData {
hasBirthday = hasBirthdayToday(cachedData: cachedUserData)
}
let controller = self.context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: hasBirthday, completion: nil)
self.push(controller) self.push(controller)
}) })
}, requestMessageUpdate: { [weak self] id, scroll in }, requestMessageUpdate: { [weak self] id, scroll in

View File

@ -226,7 +226,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions)) sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions))
} }
displayTopPeers = .custom(showSelf: false, sections: sections) displayTopPeers = .custom(showSelf: false, selfSubtitle: nil, sections: sections)
} else { } else {
displayTopPeers = .recent displayTopPeers = .recent
} }

View File

@ -67,7 +67,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
var excludeSelf = true var excludeSelf = true
let displayTopPeers: ContactListPresentation.TopPeers let displayTopPeers: ContactListPresentation.TopPeers
if case let .starsGifting(birthdays, hasActions, showSelf) = mode { if case let .starsGifting(birthdays, hasActions, showSelf, selfSubtitle) = mode {
if showSelf { if showSelf {
excludeSelf = false excludeSelf = false
} }
@ -98,7 +98,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions)) sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions))
} }
displayTopPeers = .custom(showSelf: showSelf, sections: sections) displayTopPeers = .custom(showSelf: showSelf, selfSubtitle: selfSubtitle, sections: sections)
} else { } else {
displayTopPeers = .recent displayTopPeers = .recent
} }

View File

@ -1220,7 +1220,14 @@ func openResolvedUrlImpl(
updateExternalController(nil) updateExternalController(nil)
} }
} }
let controller = context.sharedContext.makeGiftViewScreen(context: context, gift: gift, shareStory: nil, dismissed: { let controller = context.sharedContext.makeGiftViewScreen(context: context, gift: gift, shareStory: { [weak navigationController] uniqueGift in
Queue.mainQueue().after(0.15) {
if let lastController = navigationController?.viewControllers.last as? ViewController {
let controller = context.sharedContext.makeStorySharingScreen(context: context, subject: .gift(gift), parentController: lastController)
navigationController?.pushViewController(controller)
}
}
}, dismissed: {
dismissedImpl?() dismissedImpl?()
}) })
navigationController?.pushViewController(controller) navigationController?.pushViewController(controller)

View File

@ -2352,7 +2352,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var presentBirthdayPickerImpl: (() -> Void)? var presentBirthdayPickerImpl: (() -> Void)?
let starsMode: ContactSelectionControllerMode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: false) let starsMode: ContactSelectionControllerMode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: false, selfSubtitle: nil)
let contactOptions: Signal<[ContactListAdditionalOption], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) let contactOptions: Signal<[ContactListAdditionalOption], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId))
|> map { birthday in |> map { birthday in
@ -2428,22 +2428,26 @@ public final class SharedAccountContextImpl: SharedAccountContext {
var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? var currentBirthdays: [EnginePeer.Id: TelegramBirthday]?
if case let .starGiftTransfer(birthdays, _, _, _, _, showSelf) = source { if case let .starGiftTransfer(birthdays, _, _, _, _, showSelf) = source {
mode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: showSelf) mode = .starsGifting(birthdays: birthdays, hasActions: false, showSelf: showSelf, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_TransferSelf)
currentBirthdays = birthdays currentBirthdays = birthdays
} else if case let .chatList(birthdays) = source { } else if case let .chatList(birthdays) = source {
mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true) mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf)
currentBirthdays = birthdays currentBirthdays = birthdays
} else if case let .settings(birthdays) = source { } else if case let .settings(birthdays) = source {
mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true) mode = .starsGifting(birthdays: birthdays, hasActions: true, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf)
currentBirthdays = birthdays currentBirthdays = birthdays
} else { } else {
mode = .starsGifting(birthdays: nil, hasActions: true, showSelf: false) mode = .starsGifting(birthdays: nil, hasActions: true, showSelf: false, selfSubtitle: nil)
} }
var allowChannelsInSearch = false var allowChannelsInSearch = false
var isChannelGift = false
let contactOptions: Signal<[ContactListAdditionalOption], NoError> let contactOptions: Signal<[ContactListAdditionalOption], NoError>
if case let .starGiftTransfer(_, _, _, _, canExportDate, _) = source { if case let .starGiftTransfer(_, reference, _, _, canExportDate, _) = source {
allowChannelsInSearch = true allowChannelsInSearch = true
if case let .peer(peerId, _) = reference, peerId.namespace == Namespaces.Peer.CloudChannel {
isChannelGift = true
}
var subtitle: String? var subtitle: String?
if let canExportDate { if let canExportDate {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
@ -2605,9 +2609,16 @@ public final class SharedAccountContextImpl: SharedAccountContext {
case .requestPassword: case .requestPassword:
let alertController = confirmGiftWithdrawalController(context: context, reference: reference, present: { [weak controller] c, a in let alertController = confirmGiftWithdrawalController(context: context, reference: reference, present: { [weak controller] c, a in
controller?.present(c, in: .window(.root)) controller?.present(c, in: .window(.root))
}, completion: { url in }, completion: { [weak controller] url in
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
guard let controller, let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is ContactSelectionController) }
navigationController.setViewControllers(controllers, animated: true)
}) })
controller?.present(alertController, in: .window(.root)) controller?.present(alertController, in: .window(.root))
default: default:
@ -2646,6 +2657,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
} }
var controllers = navigationController.viewControllers var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is ContactSelectionController) } controllers = controllers.filter { !($0 is ContactSelectionController) }
if !isChannelGift {
var foundController = false var foundController = false
for controller in controllers.reversed() { for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation { if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation {
@ -2660,6 +2673,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
controllers.append(chatController) controllers.append(chatController)
} }
navigationController.setViewControllers(controllers, animated: true) navigationController.setViewControllers(controllers, animated: true)
}
Queue.mainQueue().after(0.3) { Queue.mainQueue().after(0.3) {
let tooltipController = UndoOverlayController( let tooltipController = UndoOverlayController(