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

View File

@ -573,7 +573,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
index += 1
}
}
case let .custom(showSelf, sections):
case let .custom(showSelf, selfSubtitle, sections):
if !topPeers.isEmpty {
var index: Int = 0
@ -637,7 +637,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
if showSelf, let accountPeer {
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)
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))
}
}
@ -882,7 +882,7 @@ public enum ContactListPresentation {
public enum TopPeers {
case none
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])
@ -1728,7 +1728,7 @@ public final class ContactListNode: ASDisplayNode {
return .single([])
}
}
case let .custom(showSelf, sections):
case let .custom(showSelf, _, sections):
var peerIds: [EnginePeer.Id] = []
if showSelf {
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) {
var intrinsicInsets = intrinsicInsets
if intrinsicInsets.top.isZero {
intrinsicInsets.top = statusBarHeight
}
let layout = ContainerViewLayout(
size: size,
metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact, orientation: nil),

View File

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

View File

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

View File

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

View File

@ -1124,6 +1124,9 @@ private final class ProfileGiftsContextImpl {
}
if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) {
self.filteredGifts[index] = self.filteredGifts[index].withSavedToProfile(added)
if !self.filter.contains(.hidden) && !added {
self.filteredGifts.remove(at: index)
}
}
self.pushState()
}
@ -1803,3 +1806,14 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer
|> 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 func presentGiftTooltip() {
guard let context = self.context, !self.displayedGiftTooltip else {
guard let context = self.context, !self.displayedGiftTooltip, let parentController = self.interfaceInteraction?.chatController() else {
return
}
self.displayedGiftTooltip = true
@ -332,7 +332,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
let _ = ApplicationSpecificNotice.incrementChannelSendGiftTooltip(accountManager: context.sharedContext.accountManager).start()
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 presentationData = context.sharedContext.currentPresentationData.with { $0 }

View File

@ -134,11 +134,15 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
if let peerStatus = title.peerStatus {
let statusContent: EmojiStatusComponent.Content
var statusParticleColor: UIColor?
switch peerStatus {
case .premium:
statusContent = .premium(color: self.theme.list.itemAccentColor)
case let .emoji(emoji):
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))
case let .emoji(emojiStatus):
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
@ -164,6 +168,7 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation
animationCache: self.animationCache,
animationRenderer: self.animationRenderer,
content: statusContent,
particleColor: statusParticleColor,
isVisibleForAnimations: true,
action: { [weak self] in
guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else {

View File

@ -151,7 +151,7 @@ private enum ChatTitleCredibilityIcon: Equatable {
case scam
case verified
case premium
case emojiStatus(PeerEmojiStatus, Int32?)
case emojiStatus(PeerEmojiStatus)
}
public final class ChatTitleView: UIView, NavigationBarTitleView {
@ -275,7 +275,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
} else if peer.isScam {
titleCredibilityIcon = .scam
} else if let emojiStatus = peer.emojiStatus, !premiumConfiguration.isPremiumDisabled {
titleStatusIcon = .emojiStatus(emojiStatus, emojiStatus.color)
titleStatusIcon = .emojiStatus(emojiStatus)
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
titleCredibilityIcon = .premium
}
@ -284,7 +284,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView {
titleCredibilityIcon = .verified
}
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())
case .scam:
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))
}
@ -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())
case .scam:
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))
}
let titleStatusContent: EmojiStatusComponent.Content
var titleStatusParticleColor: UIColor?
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))
if let color {
if let color = emojiStatus.color {
titleStatusParticleColor = UIColor(rgb: UInt32(bitPattern: color))
}
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>
var previewScreenTransition = transition
if let current = self.previewScreenView {
@ -1190,58 +1190,66 @@ public final class EmojiStatusSelectionController: ViewController {
return
}
if let result = result, let previewItem = strongSelf.previewItem {
var emojiString: String?
if let itemFile = previewItem.item.itemFile {
attributeLoop: for attribute in itemFile.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
emojiString = alt
break attributeLoop
default:
break
}
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)
}
}
let context = strongSelf.context
let _ = (context.engine.stickers.availableReactions()
|> take(1)
|> mapToSignal { availableReactions -> Signal<String?, NoError> in
guard let emojiString = emojiString, let availableReactions = availableReactions else {
return .single(nil)
}
for reaction in availableReactions.reactions {
if case let .builtin(value) = reaction.value, value == emojiString {
if let aroundAnimation = reaction.aroundAnimation {
return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource)
|> take(1)
|> map { data -> String? in
if data.complete {
return data.path
} else {
return nil
}
}
} else {
return .single(nil)
} else {
var emojiString: String?
if let itemFile = previewItem.item.itemFile {
attributeLoop: for attribute in itemFile.attributes {
switch attribute {
case let .CustomEmoji(_, _, alt, _):
emojiString = alt
break attributeLoop
default:
break
}
}
}
return .single(nil)
}
|> deliverOnMainQueue).start(next: { filePath in
guard let strongSelf = self, let previewItem = strongSelf.previewItem, let destinationView = strongSelf.controller?.destinationItemView() else {
return
let context = strongSelf.context
let _ = (context.engine.stickers.availableReactions()
|> take(1)
|> mapToSignal { availableReactions -> Signal<String?, NoError> in
guard let emojiString = emojiString, let availableReactions = availableReactions else {
return .single(nil)
}
for reaction in availableReactions.reactions {
if case let .builtin(value) = reaction.value, value == emojiString {
if let aroundAnimation = reaction.aroundAnimation {
return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource)
|> take(1)
|> map { data -> String? in
if data.complete {
return data.path
} else {
return nil
}
}
} else {
return .single(nil)
}
}
}
return .single(nil)
}
let expirationDate: Int32? = result.timestamp
let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate)
|> deliverOnMainQueue).start()
strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: filePath, destinationView: destinationView, fromBackground: true)
})
|> deliverOnMainQueue).start(next: { filePath in
guard let strongSelf = self, let previewItem = strongSelf.previewItem, let destinationView = strongSelf.controller?.destinationItemView() else {
return
}
let _ = (strongSelf.context.engine.accountData.setEmojiStatus(file: previewItem.item.itemFile, expirationDate: expirationDate)
|> deliverOnMainQueue).start()
strongSelf.animateOutToStatus(item: previewItem.item, sourceLayer: result.sourceView.layer, customEffectFile: filePath, destinationView: destinationView, fromBackground: true)
})
}
} else {
strongSelf.dismissedPreviewItem = strongSelf.previewItem
strongSelf.previewItem = nil
@ -1551,3 +1559,15 @@ private func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to tar
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 {
if !itemGroup.items.isEmpty {
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] = [
"recent": .recent,
"liked": .liked,
"collectible": .collectible
]
let titleMapping: [String: String] = [
"recent": component.strings.Stickers_Recent,
"liked": "",
"collectible": ""
]
if let icon = iconMapping[id], let title = titleMapping[id] {
topEmojiItems.append(EntityKeyboardTopPanelComponent.Item(

View File

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

View File

@ -87,6 +87,7 @@ public final class GiftItemComponent: Component {
case profile
case thumbnail
case preview
case grid
}
let context: AccountContext
@ -99,6 +100,7 @@ public final class GiftItemComponent: Component {
let isLoading: Bool
let isHidden: Bool
let isSoldOut: Bool
let isSelected: Bool
let mode: Mode
public init(
@ -112,6 +114,7 @@ public final class GiftItemComponent: Component {
isLoading: Bool = false,
isHidden: Bool = false,
isSoldOut: Bool = false,
isSelected: Bool = false,
mode: Mode = .generic
) {
self.context = context
@ -124,6 +127,7 @@ public final class GiftItemComponent: Component {
self.isLoading = isLoading
self.isHidden = isHidden
self.isSoldOut = isSoldOut
self.isSelected = isSelected
self.mode = mode
}
@ -158,6 +162,9 @@ public final class GiftItemComponent: Component {
if lhs.isSoldOut != rhs.isSoldOut {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.mode != rhs.mode {
return false
}
@ -181,6 +188,7 @@ public final class GiftItemComponent: Component {
private let ribbonText = ComponentView<Empty>()
private var animationLayer: InlineStickerItemLayer?
private var selectionLayer: SimpleShapeLayer?
private var disposables = DisposableSet()
private var fetchedFiles = Set<Int64>()
@ -234,6 +242,10 @@ public final class GiftItemComponent: Component {
size = CGSize(width: availableSize.width, height: availableSize.width)
iconSize = CGSize(width: floor(size.width * 0.7), height: floor(size.width * 0.7))
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:
size = availableSize
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
}
}

View File

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

View File

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

View File

@ -233,7 +233,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
case let .starGift(gift):
media = [
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 GiftViewScreen
import UndoUI
import ConfettiEffect
final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -377,6 +378,8 @@ final class GiftSetupScreenComponent: Component {
action: { _ in return true }
)
(navigationController.viewControllers.last as? ViewController)?.present(tooltipController, in: .current)
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
}
if let completion {

View File

@ -47,6 +47,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let cancel: (Bool) -> Void
let openPeer: (EnginePeer) -> Void
let openAddress: (String) -> Void
let copyAddress: (String) -> Void
let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void
let openStarsIntro: () -> Void
@ -66,6 +67,7 @@ private final class GiftViewSheetContent: CombinedComponent {
cancel: @escaping (Bool) -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openAddress: @escaping (String) -> Void,
copyAddress: @escaping (String) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void,
@ -84,6 +86,7 @@ private final class GiftViewSheetContent: CombinedComponent {
self.cancel = cancel
self.openPeer = openPeer
self.openAddress = openAddress
self.copyAddress = copyAddress
self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
@ -447,6 +450,7 @@ private final class GiftViewSheetContent: CombinedComponent {
var soldOut = false
var nameHidden = false
var upgraded = false
var exported = false
var canUpgrade = false
var upgradeStars: Int64?
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
return (TelegramTextAttributes.URL, contents)
})
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
@ -1286,6 +1292,8 @@ private final class GiftViewSheetContent: CombinedComponent {
)
))
case let .address(address):
exported = true
func formatAddress(_ str: String) -> String {
var result = str
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)
),
action: {
component.openAddress(address)
component.cancel(true)
component.copyAddress(address)
}
)
)
@ -1403,6 +1410,8 @@ private final class GiftViewSheetContent: CombinedComponent {
var canTransfer = true
if let peer = state.peerMap[peerId], case let .channel(channel) = peer, !channel.flags.contains(.isCreator) {
canTransfer = false
} else if subject.arguments?.transferStars == nil {
canTransfer = false
}
let buttonsCount = canTransfer ? 3 : 2
@ -1871,13 +1880,17 @@ private final class GiftViewSheetContent: CombinedComponent {
originY += table.size.height + 23.0
}
if incoming && !converted && !upgraded && !showUpgradePreview && !showWearPreview {
if ((incoming && !converted && !upgraded) || exported) && (!showUpgradePreview && !showWearPreview) {
let linkColor = theme.actionSheet.controlAccentColor
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
}
let descriptionText: String
if savedToProfile {
var addressToOpen: String?
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
} else if let upgradeStars, upgradeStars > 0 && !upgraded {
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
return (TelegramTextAttributes.URL, contents)
})
descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]")
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
@ -1917,10 +1932,15 @@ private final class GiftViewSheetContent: CombinedComponent {
},
tapAction: { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
component.updateSavedToProfile(!savedToProfile)
Queue.mainQueue().after(0.6, {
component.cancel(false)
})
if let addressToOpen {
component.openAddress(addressToOpen)
component.cancel(true)
} else {
component.updateSavedToProfile(!savedToProfile)
Queue.mainQueue().after(0.6, {
component.cancel(false)
})
}
}
}
),
@ -2201,6 +2221,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
let subject: GiftViewScreen.Subject
let openPeer: (EnginePeer) -> Void
let openAddress: (String) -> Void
let copyAddress: (String) -> Void
let updateSavedToProfile: (Bool) -> Void
let convertToStars: () -> Void
let openStarsIntro: () -> Void
@ -2218,6 +2239,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
subject: GiftViewScreen.Subject,
openPeer: @escaping (EnginePeer) -> Void,
openAddress: @escaping (String) -> Void,
copyAddress: @escaping (String) -> Void,
updateSavedToProfile: @escaping (Bool) -> Void,
convertToStars: @escaping () -> Void,
openStarsIntro: @escaping () -> Void,
@ -2234,6 +2256,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
self.subject = subject
self.openPeer = openPeer
self.openAddress = openAddress
self.copyAddress = copyAddress
self.updateSavedToProfile = updateSavedToProfile
self.convertToStars = convertToStars
self.openStarsIntro = openStarsIntro
@ -2286,6 +2309,7 @@ private final class GiftViewSheetComponent: CombinedComponent {
},
openPeer: context.component.openPeer,
openAddress: context.component.openAddress,
copyAddress: context.component.copyAddress,
updateSavedToProfile: context.component.updateSavedToProfile,
convertToStars: context.component.convertToStars,
openStarsIntro: context.component.openStarsIntro,
@ -2447,6 +2471,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
var openPeerImpl: ((EnginePeer) -> Void)?
var openAddressImpl: ((String) -> Void)?
var copyAddressImpl: ((String) -> Void)?
var updateSavedToProfileImpl: ((Bool) -> Void)?
var convertToStarsImpl: (() -> Void)?
var openStarsIntroImpl: (() -> Void)?
@ -2470,6 +2495,9 @@ public class GiftViewScreen: ViewControllerComponentContainer {
openAddress: { address in
openAddressImpl?(address)
},
copyAddress: { address in
copyAddressImpl?(address)
},
updateSavedToProfile: { added in
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
guard let self, let arguments = self.subject.arguments, let reference = arguments.reference else {
return
@ -2618,6 +2658,11 @@ public class GiftViewScreen: ViewControllerComponentContainer {
let configuration = GiftConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
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)
if currentTime > starsConvertMaxDate {
let days: Int32 = Int32(ceil(Float(configuration.convertToStarsPeriod) / 86400.0))
@ -2653,8 +2698,10 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if let navigationController {
Queue.mainQueue().after(0.5) {
if let starsContext = context.starsContext {
navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true)
if !isChannelGift {
if let starsContext = context.starsContext {
navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true)
}
}
if let lastController = navigationController.viewControllers.last as? ViewController {

View File

@ -127,7 +127,7 @@ final class MediaEditorComposer {
if values.isSticker {
self.maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97)))
} 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]) {
@ -227,7 +227,7 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp
if values.isSticker {
maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97)))
} 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 {
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))
if values.isSticker || values.isAvatar {
if values.isSticker {
let minSize = min(dimensions.width, dimensions.height)
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))
} 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 {
let minSize = min(dimensions.width, dimensions.height)
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 {
let stickerFrameFraction: CGFloat
switch controller.mode {
case .avatarEditor, .stickerEditor:
case .stickerEditor:
stickerFrameFraction = 0.97
default:
stickerFrameFraction = 1.0

View File

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

View File

@ -1173,7 +1173,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
tabsAlpha = 1.0 - tabsOffset / tabsHeight
}
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.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)))
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
var icons: [TelegramMediaFile] = []
switch key {

View File

@ -8906,7 +8906,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
private func editingOpenNameColorSetup() {
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)
} 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))
@ -9815,16 +9815,23 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
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) }
var hasBirthday = false
if let cachedUserData = self.data?.cachedData as? CachedUserData {
hasBirthday = hasBirthdayToday(cachedData: cachedUserData)
}
let controller = self.context.sharedContext.makeGiftOptionsController(
context: self.context,
peerId: self.peerId,
premiumOptions: premiumOptions,
hasBirthday: false,
hasBirthday: hasBirthday,
completion: { [weak self] in
guard let self, let profileGiftsContext = self.data?.profileGiftsContext else {
return
}
profileGiftsContext.reload()
Queue.mainQueue().after(0.5) {
profileGiftsContext.reload()
}
}
)
self.controller?.push(controller)
@ -10916,7 +10923,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let controller = self.controller else {
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
}
@ -10979,18 +10986,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
toggleFilter(.unique)
})))
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Displayed, icon: { theme in
return filter.contains(.displayed) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, f in
toggleFilter(.displayed)
})))
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Hidden, icon: { theme in
return filter.contains(.hidden) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, f in
toggleFilter(.hidden)
})))
if channel.hasPermission(.sendSomething) {
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Displayed, icon: { theme in
return filter.contains(.displayed) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, f in
toggleFilter(.displayed)
})))
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Hidden, icon: { theme in
return filter.contains(.hidden) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, f in
toggleFilter(.hidden)
})))
}
return ContextController.Items(content: .list(items))
}
@ -12019,7 +12028,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true))
}
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))
}
default:
@ -12294,14 +12303,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
self.didPlayBirthdayAnimation = true
var hasBirthdayToday = false
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 {
if hasBirthdayToday(cachedData: cachedData) {
Queue.mainQueue().after(0.3) {
var birthdayItemFrame: CGRect?
if let section = self.regularSections[InfoSection.peerInfo] {

View File

@ -9,6 +9,7 @@ import AccountContext
import ContextUI
import PhotoResources
import TelegramUIPreferences
import TelegramStringFormatting
import ItemListPeerItem
import ItemListPeerActionItem
import MergeLists
@ -48,6 +49,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private var panelButton: SolidRoundedButtonNode?
private var panelCheck: ComponentView<Empty>?
private let emptyResultsClippingView = UIView()
private let emptyResultsAnimation = ComponentView<Empty>()
private let emptyResultsTitle = ComponentView<Empty>()
private let emptyResultsAction = ComponentView<Empty>()
@ -93,7 +95,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.addSubnode(self.backgroundNode)
self.addSubnode(self.scrollNode)
self.dataDisposable = (profileGifts.state
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
guard let self else {
@ -125,6 +127,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
self.scrollNode.view.delegate = self
self.emptyResultsClippingView.clipsToBounds = true
self.scrollNode.view.addSubview(self.emptyResultsClippingView)
}
public func ensureMessageIsVisible(id: MessageId) {
@ -145,7 +150,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let optionSpacing: CGFloat = 10.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 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)
@ -489,6 +494,11 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let emptyAnimationSpacing: CGFloat = 20.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(
transition: .immediate,
component: AnyComponent(
@ -517,7 +527,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return
}
self.profileGifts.updateFilter(.All)
}
},
animateScale: false
)
),
environment: {},
@ -545,7 +556,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if view.superview == nil {
view.alpha = 0.0
fadeTransition.setAlpha(view: view, alpha: 1.0)
self.scrollNode.view.addSubview(view)
self.emptyResultsClippingView.addSubview(view)
view.playOnce()
}
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
@ -555,7 +566,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if view.superview == nil {
view.alpha = 0.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)
transition.setPosition(view: view, position: emptyResultsTitleFrame.center)
@ -564,7 +575,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if view.superview == nil {
view.alpha = 0.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)
transition.setPosition(view: view, position: emptyResultsActionFrame.center)
@ -572,6 +583,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} else {
if let view = self.emptyResultsAnimation.view {
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
self.emptyResultsClippingView.isHidden = true
view.removeFromSuperview()
})
}
@ -650,8 +662,21 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.chatControllerInteraction.navigationController()?.pushViewController(controller)
})
} else {
let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: self.peerId, premiumOptions: [], hasBirthday: false, completion: nil)
self.chatControllerInteraction.navigationController()?.pushViewController(controller)
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)
})
}
}

View File

@ -52,6 +52,8 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/TelegramUI/Components/EmojiActionIconComponent",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//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))
},
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)
))),

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) {
snapshot.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: snapshot.frame.size)
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()
})
}

View File

@ -29,8 +29,9 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
let subtitleString: String?
let files: [Int64: TelegramMediaFile]
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.theme = theme
self.componentTheme = componentTheme
@ -41,6 +42,7 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
self.subtitleString = subtitleString
self.files = files
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) {
@ -102,7 +104,9 @@ final class PeerNameColorProfilePreviewItem: ListViewItem, ItemListItem, ListIte
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false
}
if lhs.showBackground != rhs.showBackground {
return false
}
return true
}
}
@ -114,6 +118,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
private let subtitle = ComponentView<Empty>()
private var icon: ComponentView<Empty>?
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
@ -121,6 +126,9 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
private var item: PeerNameColorProfilePreviewItem?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
@ -137,10 +145,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
self.clipsToBounds = true
self.isUserInteractionEnabled = false
}
deinit {
}
func asyncLayout() -> (_ item: PeerNameColorProfilePreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
return { [weak self] item, params, neighbors in
let separatorHeight = UIScreenPixel
@ -158,15 +163,19 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
guard let self else {
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: {
})
}
self.item = item
self.backgroundNode.backgroundColor = item.theme.rootController.navigationBar.opaqueBackgroundColor
self.topStripeNode.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 {
self.addSubnode(self.topStripeNode)
}
@ -178,10 +187,19 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
}
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.bottomStripeNode.isHidden = true
self.bottomStripeNode.isHidden = false
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 {
self.backgroundNode.isHidden = true
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
@ -219,11 +237,19 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
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 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(
transition: .immediate,
component: AnyComponent(PeerInfoCoverComponent(
context: item.context,
subject: item.peer.flatMap { .peer($0) },
subject: subject,
files: item.files,
isDark: item.theme.overallDarkAppearance,
avatarCenter: avatarFrame.center,
@ -238,7 +264,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
backgroundView.clipsToBounds = true
self.view.insertSubview(backgroundView, at: 0)
self.view.insertSubview(backgroundView, at: 1)
}
backgroundView.frame = coverFrame
}
@ -296,7 +322,9 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
}
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
} else {
statusColor = item.theme.list.itemCheckColors.fillColor
@ -321,7 +349,13 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
let backgroundColor: UIColor
let titleColor: 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
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)
@ -384,6 +418,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
animationCache: item.context.animationCache,
animationRenderer: item.context.animationRenderer,
content: emojiStatusContent,
particleColor: particleColor,
isVisibleForAnimations: true,
action: nil
)),
@ -424,6 +459,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
}
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()
}
} 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)
}
}, openPremiumRequiredForMessaging: { [weak self] in

View File

@ -4577,7 +4577,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
let controller = self.context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: false, 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)
})
}, 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))
}
displayTopPeers = .custom(showSelf: false, sections: sections)
displayTopPeers = .custom(showSelf: false, selfSubtitle: nil, sections: sections)
} else {
displayTopPeers = .recent
}

View File

@ -67,7 +67,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
var excludeSelf = true
let displayTopPeers: ContactListPresentation.TopPeers
if case let .starsGifting(birthdays, hasActions, showSelf) = mode {
if case let .starsGifting(birthdays, hasActions, showSelf, selfSubtitle) = mode {
if showSelf {
excludeSelf = false
}
@ -98,7 +98,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
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 {
displayTopPeers = .recent
}

View File

@ -1220,7 +1220,14 @@ func openResolvedUrlImpl(
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?()
})
navigationController?.pushViewController(controller)

View File

@ -2352,7 +2352,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
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))
|> map { birthday in
@ -2428,22 +2428,26 @@ public final class SharedAccountContextImpl: SharedAccountContext {
var currentBirthdays: [EnginePeer.Id: TelegramBirthday]?
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
} 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
} 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
} else {
mode = .starsGifting(birthdays: nil, hasActions: true, showSelf: false)
mode = .starsGifting(birthdays: nil, hasActions: true, showSelf: false, selfSubtitle: nil)
}
var allowChannelsInSearch = false
var isChannelGift = false
let contactOptions: Signal<[ContactListAdditionalOption], NoError>
if case let .starGiftTransfer(_, _, _, _, canExportDate, _) = source {
if case let .starGiftTransfer(_, reference, _, _, canExportDate, _) = source {
allowChannelsInSearch = true
if case let .peer(peerId, _) = reference, peerId.namespace == Namespaces.Peer.CloudChannel {
isChannelGift = true
}
var subtitle: String?
if let canExportDate {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
@ -2605,9 +2609,16 @@ public final class SharedAccountContextImpl: SharedAccountContext {
case .requestPassword:
let alertController = confirmGiftWithdrawalController(context: context, reference: reference, present: { [weak controller] c, a in
controller?.present(c, in: .window(.root))
}, completion: { url in
}, completion: { [weak controller] url in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
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))
default:
@ -2646,20 +2657,23 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is ContactSelectionController) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
if !isChannelGift {
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: peer.id) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
}
if !foundController {
let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
Queue.mainQueue().after(0.3) {
let tooltipController = UndoOverlayController(