mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit '66f78a352a' into story-folders
This commit is contained in:
commit
2aace71289
67 changed files with 6083 additions and 1338 deletions
|
|
@ -14690,3 +14690,61 @@ Sorry for the inconvenience.";
|
|||
"Chat.ReplyPanel.ReplyToTodoItem" = "Reply to Checklist Item";
|
||||
|
||||
"Chat.Todo.ReplyToItem" = "Reply to Item";
|
||||
|
||||
"Premium.SubscribeForBiannual" = "Subscribe for %@ / 2 years";
|
||||
"Premium.Biannual" = "2 Years";
|
||||
|
||||
"Premium.PricePer2Years" = "%@/2 years";
|
||||
|
||||
"Chat.SensitiveContentShort" = "18+";
|
||||
|
||||
"AccessDenied.AgeVerificationCamera" = "Telegram needs access to your camera for age verification.\n\nOpen your device's Settings > Privacy > Camera and set Telegram to ON.";
|
||||
|
||||
"PeerInfo.Gifts.AddGiftsButton" = "Add Gifts";
|
||||
|
||||
"PeerInfo.Gifts.AddCollection" = "Add Collection";
|
||||
"PeerInfo.Gifts.AddGifts" = "Add Gifts";
|
||||
"PeerInfo.Gifts.RenameCollection" = "Rename";
|
||||
"PeerInfo.Gifts.ShareCollection" = "Share";
|
||||
"PeerInfo.Gifts.DeleteCollection" = "Delete Collection";
|
||||
|
||||
"PeerInfo.Gifts.CreateCollection.Title" = "Create a New Collection";
|
||||
"PeerInfo.Gifts.CreateCollection.Text" = "Choose a name for your collection and start adding your gifts there.";
|
||||
"PeerInfo.Gifts.CreateCollection.Placeholder" = "Title";
|
||||
|
||||
"PeerInfo.Gifts.RenameCollection.Title" = "Create a New Collection";
|
||||
|
||||
"PeerInfo.Gifts.Context.AddToCollection" = "Add to Collection";
|
||||
"PeerInfo.Gifts.Context.NewCollection" = "New Collection";
|
||||
"PeerInfo.Gifts.Context.RemoveFromCollection" = "Remove from Collection";
|
||||
|
||||
"PeerInfo.Gifts.CollectionLimitReached.Title" = "Limit Reached";
|
||||
"PeerInfo.Gifts.CollectionLimitReached.Text" = "Please remove one of the existing collections to add a new one.";
|
||||
|
||||
"PeerInfo.Gifts.RemoveCollectionConfirmation" = "This will remove the collection.";
|
||||
"PeerInfo.Gifts.RemoveCollectionAction" = "Remove";
|
||||
|
||||
"PeerInfo.Gifts.EmptyCollection.Title" = "Organize Your Gifts";
|
||||
"PeerInfo.Gifts.EmptyCollection.Text" = "Add some gifts to this collection.";
|
||||
"PeerInfo.Gifts.EmptyCollection.Action" = "Add to Collection";
|
||||
|
||||
"AddGifts.Title" = "Add Gifts";
|
||||
"AddGifts.AddGifts_1" = "Add %@ Gift";
|
||||
"AddGifts.AddGifts_any" = "Add %@ Gifts";
|
||||
|
||||
"Gift.Options.Gift.BuyLimitReached_1" = "You've already sent %@ of these gifts, and it's the limit.";
|
||||
"Gift.Options.Gift.BuyLimitReached_any" = "You've already sent %@ of these gifts, and it's the limit.";
|
||||
|
||||
"AgeVerification.Title" = "Age Verification";
|
||||
"AgeVerification.Text" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
|
||||
"AgeVerification.Text.gb" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
|
||||
"AgeVerification.Verify" = "Verify My Age";
|
||||
|
||||
"AgeVerification.Success.Title" = "Age check passed!";
|
||||
"AgeVerification.Success.Text" = "You can now view this content.";
|
||||
|
||||
"AgeVerification.Fail.Title" = "Age check failed";
|
||||
"AgeVerification.Fail.Text" = "Sorry, you can't view this content.";
|
||||
|
||||
"FaceScan.Instruction.Position" = "Position your face\nwithin the frame";
|
||||
"FaceScan.Instruction.Rotate" = "Move your head slowly to\ncomplete the circle";
|
||||
|
|
|
|||
|
|
@ -543,12 +543,13 @@ public final class NavigateToChatControllerParams {
|
|||
public let changeColors: Bool
|
||||
public let setupController: (ChatController) -> Void
|
||||
public let completion: (ChatController) -> Void
|
||||
public let chatListCompletion: ((ChatListController) -> Void)?
|
||||
public let chatListCompletion: ((ChatListController) -> Void)
|
||||
public let pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)?
|
||||
public let forceOpenChat: Bool
|
||||
public let customChatNavigationStack: [EnginePeer.Id]?
|
||||
public let skipAgeVerification: Bool
|
||||
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: NavigateToChatControllerParams.ReportReason? = nil, animated: Bool = true, forceAnimatedScroll: Bool = false, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }, chatListCompletion: @escaping (ChatListController) -> Void = { _ in }, forceOpenChat: Bool = false, customChatNavigationStack: [EnginePeer.Id]? = nil) {
|
||||
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: Location, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: NavigateToChatControllerParams.ReportReason? = nil, animated: Bool = true, forceAnimatedScroll: Bool = false, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, pushController: ((ChatController, Bool, @escaping () -> Void) -> Void)? = nil, completion: @escaping (ChatController) -> Void = { _ in }, chatListCompletion: @escaping (ChatListController) -> Void = { _ in }, forceOpenChat: Bool = false, customChatNavigationStack: [EnginePeer.Id]? = nil, skipAgeVerification: Bool = false) {
|
||||
self.navigationController = navigationController
|
||||
self.chatController = chatController
|
||||
self.chatLocationContextHolder = chatLocationContextHolder
|
||||
|
|
@ -582,6 +583,46 @@ public final class NavigateToChatControllerParams {
|
|||
self.chatListCompletion = chatListCompletion
|
||||
self.forceOpenChat = forceOpenChat
|
||||
self.customChatNavigationStack = customChatNavigationStack
|
||||
self.skipAgeVerification = skipAgeVerification
|
||||
}
|
||||
|
||||
public func withSkipAgeVerification(_ skipAgeVerification: Bool) -> NavigateToChatControllerParams {
|
||||
return NavigateToChatControllerParams(
|
||||
navigationController: self.navigationController,
|
||||
chatController: self.chatController,
|
||||
context: self.context,
|
||||
chatLocation: self.chatLocation,
|
||||
chatLocationContextHolder: self.chatLocationContextHolder,
|
||||
subject: self.subject,
|
||||
botStart: self.botStart,
|
||||
attachBotStart: self.attachBotStart,
|
||||
botAppStart: self.botAppStart,
|
||||
updateTextInputState: self.updateTextInputState,
|
||||
activateInput: self.activateInput,
|
||||
keepStack: self.keepStack,
|
||||
useExisting: self.useExisting,
|
||||
useBackAnimation: self.useBackAnimation,
|
||||
purposefulAction: self.purposefulAction,
|
||||
scrollToEndIfExists: self.scrollToEndIfExists,
|
||||
activateMessageSearch: self.activateMessageSearch,
|
||||
peekData: self.peekData,
|
||||
peerNearbyData: self.peerNearbyData,
|
||||
reportReason: self.reportReason,
|
||||
animated: self.animated,
|
||||
forceAnimatedScroll: self.forceAnimatedScroll,
|
||||
options: self.options,
|
||||
parentGroupId: self.parentGroupId,
|
||||
chatListFilter: self.chatListFilter,
|
||||
chatNavigationStack: self.chatNavigationStack,
|
||||
changeColors: self.changeColors,
|
||||
setupController: self.setupController,
|
||||
pushController: self.pushController,
|
||||
completion: self.completion,
|
||||
chatListCompletion: self.chatListCompletion,
|
||||
forceOpenChat: self.forceOpenChat,
|
||||
customChatNavigationStack: self.customChatNavigationStack,
|
||||
skipAgeVerification: skipAgeVerification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -760,7 +760,17 @@ public final class Camera {
|
|||
|
||||
public let metrics: Camera.Metrics
|
||||
|
||||
public init(configuration: Camera.Configuration = Configuration(preset: .hd1920x1080, position: .back, audio: true, photo: false, metadata: false), previewView: CameraSimplePreviewView? = nil, secondaryPreviewView: CameraSimplePreviewView? = nil) {
|
||||
public init(
|
||||
configuration: Camera.Configuration = Configuration(
|
||||
preset: .hd1920x1080,
|
||||
position: .back,
|
||||
audio: true,
|
||||
photo: false,
|
||||
metadata: false
|
||||
),
|
||||
previewView: CameraSimplePreviewView? = nil,
|
||||
secondaryPreviewView: CameraSimplePreviewView? = nil
|
||||
) {
|
||||
Logger.shared.log("Camera", "Init")
|
||||
|
||||
self.metrics = Camera.Metrics(model: DeviceModel.current)
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ private final class ItemNode: ASDisplayNode {
|
|||
if self.isReordering != isReordering {
|
||||
self.isReordering = isReordering
|
||||
if self.isReordering {
|
||||
self.startShaking()
|
||||
self.layer.addReorderingShaking()
|
||||
} else {
|
||||
self.layer.removeAnimation(forKey: "shaking_position")
|
||||
self.layer.removeAnimation(forKey: "shaking_rotation")
|
||||
|
|
@ -414,52 +414,7 @@ private final class ItemNode: ASDisplayNode {
|
|||
transition.updateSublayerTransformScale(node: self.badgeContainerNode, scale: 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
private func startShaking() {
|
||||
func degreesToRadians(_ x: CGFloat) -> CGFloat {
|
||||
return .pi * x / 180.0
|
||||
}
|
||||
|
||||
let duration: Double = 0.4
|
||||
let displacement: CGFloat = 1.0
|
||||
let degreesRotation: CGFloat = 2.0
|
||||
|
||||
let negativeDisplacement = -1.0 * displacement
|
||||
let position = CAKeyframeAnimation.init(keyPath: "position")
|
||||
position.beginTime = 0.8
|
||||
position.duration = duration
|
||||
position.values = [
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
|
||||
NSValue(cgPoint: CGPoint(x: 0, y: 0)),
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
|
||||
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
|
||||
]
|
||||
position.calculationMode = .linear
|
||||
position.isRemovedOnCompletion = false
|
||||
position.repeatCount = Float.greatestFiniteMagnitude
|
||||
position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
||||
position.isAdditive = true
|
||||
|
||||
let transform = CAKeyframeAnimation.init(keyPath: "transform")
|
||||
transform.beginTime = 2.6
|
||||
transform.duration = 0.3
|
||||
transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
|
||||
transform.values = [
|
||||
degreesToRadians(-1.0 * degreesRotation),
|
||||
degreesToRadians(degreesRotation),
|
||||
degreesToRadians(-1.0 * degreesRotation)
|
||||
]
|
||||
transform.calculationMode = .linear
|
||||
transform.isRemovedOnCompletion = false
|
||||
transform.repeatCount = Float.greatestFiniteMagnitude
|
||||
transform.isAdditive = true
|
||||
transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
||||
|
||||
self.layer.add(position, forKey: "shaking_position")
|
||||
self.layer.add(transform, forKey: "shaking_rotation")
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let deleteButtonNode = self.deleteButtonNode {
|
||||
if deleteButtonNode.frame.insetBy(dx: -4.0, dy: -4.0).contains(point) {
|
||||
|
|
|
|||
|
|
@ -695,6 +695,10 @@ public struct ComponentTransition {
|
|||
}
|
||||
|
||||
public func setSublayerTransform(layer: CALayer, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||
if CATransform3DEqualToTransform(layer.sublayerTransform, transform) {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
switch self.animation {
|
||||
case .none:
|
||||
layer.sublayerTransform = transform
|
||||
|
|
|
|||
|
|
@ -824,7 +824,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||
credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased())
|
||||
} else if peer.isFake {
|
||||
credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased())
|
||||
} else if let emojiStatus = peer.emojiStatus {
|
||||
} else if let emojiStatus = peer.emojiStatus, !item.isAd {
|
||||
credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2))
|
||||
if let color = emojiStatus.color {
|
||||
credibilityParticleColor = UIColor(rgb: UInt32(bitPattern: color))
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public enum DeviceAccessCameraSubject {
|
|||
case video
|
||||
case videoCall
|
||||
case qrCode
|
||||
case ageVerification
|
||||
}
|
||||
|
||||
public enum DeviceAccessMicrophoneSubject {
|
||||
|
|
@ -332,6 +333,8 @@ public final class DeviceAccess {
|
|||
text = presentationData.strings.AccessDenied_VideoCallCamera
|
||||
case .qrCode:
|
||||
text = presentationData.strings.AccessDenied_QrCamera
|
||||
case .ageVerification:
|
||||
text = presentationData.strings.AccessDenied_AgeVerificationCamera
|
||||
}
|
||||
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
|
||||
openSettings()
|
||||
|
|
@ -356,6 +359,8 @@ public final class DeviceAccess {
|
|||
text = presentationData.strings.AccessDenied_VideoCallCamera
|
||||
case .qrCode:
|
||||
text = presentationData.strings.AccessDenied_QrCamera
|
||||
case .ageVerification:
|
||||
text = presentationData.strings.AccessDenied_AgeVerificationCamera
|
||||
}
|
||||
}
|
||||
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
|
||||
|
|
|
|||
|
|
@ -40,4 +40,49 @@ public extension CALayer {
|
|||
|
||||
self.add(animation, forKey: "shake")
|
||||
}
|
||||
|
||||
func addReorderingShaking() {
|
||||
func degreesToRadians(_ x: CGFloat) -> CGFloat {
|
||||
return .pi * x / 180.0
|
||||
}
|
||||
|
||||
let duration: Double = 0.4
|
||||
let displacement: CGFloat = 1.0
|
||||
let degreesRotation: CGFloat = 2.0
|
||||
|
||||
let negativeDisplacement = -1.0 * displacement
|
||||
let position = CAKeyframeAnimation.init(keyPath: "position")
|
||||
position.beginTime = 0.8
|
||||
position.duration = duration
|
||||
position.values = [
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
|
||||
NSValue(cgPoint: CGPoint(x: 0, y: 0)),
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
|
||||
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
|
||||
]
|
||||
position.calculationMode = .linear
|
||||
position.isRemovedOnCompletion = false
|
||||
position.repeatCount = Float.greatestFiniteMagnitude
|
||||
position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
||||
position.isAdditive = true
|
||||
|
||||
let transform = CAKeyframeAnimation.init(keyPath: "transform")
|
||||
transform.beginTime = 2.6
|
||||
transform.duration = 0.3
|
||||
transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
|
||||
transform.values = [
|
||||
degreesToRadians(-1.0 * degreesRotation),
|
||||
degreesToRadians(degreesRotation),
|
||||
degreesToRadians(-1.0 * degreesRotation)
|
||||
]
|
||||
transform.calculationMode = .linear
|
||||
transform.isRemovedOnCompletion = false
|
||||
transform.repeatCount = Float.greatestFiniteMagnitude
|
||||
transform.isAdditive = true
|
||||
transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
||||
|
||||
self.add(position, forKey: "shaking_position")
|
||||
self.add(transform, forKey: "shaking_rotation")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponentResourceContent",
|
||||
"//submodules/ImageTransparency",
|
||||
#"//submodules/GalleryUI",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/TelegramUniversalVideoContent",
|
||||
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
||||
|
|
|
|||
|
|
@ -45,9 +45,185 @@ private func appSpecificStringWithAppliedEntities(_ text: String, entities: [Mes
|
|||
return data
|
||||
}
|
||||
|
||||
private func preprocessLists(attributedString: NSAttributedString) -> NSAttributedString {
|
||||
let result = NSMutableAttributedString()
|
||||
var listCounters: [NSTextList: Int] = [:]
|
||||
|
||||
let string = attributedString.string
|
||||
var currentIndex = 0
|
||||
|
||||
while currentIndex < string.count {
|
||||
let nsRange = NSRange(location: currentIndex, length: 1)
|
||||
let attributes = attributedString.attributes(at: currentIndex, effectiveRange: nil)
|
||||
|
||||
if let paragraphStyle = attributes[.paragraphStyle] as? NSParagraphStyle,
|
||||
!paragraphStyle.textLists.isEmpty {
|
||||
let listItemRange = findListItemRange(in: attributedString, startingAt: currentIndex)
|
||||
let listItemSubstring = attributedString.attributedSubstring(from: listItemRange)
|
||||
|
||||
let listMarker = generateListMarker(for: paragraphStyle, counters: &listCounters)
|
||||
|
||||
let newAttributedString = NSMutableAttributedString()
|
||||
|
||||
let markerString = NSMutableAttributedString(string: listMarker)
|
||||
if let firstCharFont = attributes[.font] {
|
||||
markerString.addAttribute(.font, value: firstCharFont, range: NSRange(location: 0, length: listMarker.count))
|
||||
}
|
||||
|
||||
let newParagraphStyle = NSMutableParagraphStyle()
|
||||
newParagraphStyle.alignment = paragraphStyle.alignment
|
||||
newParagraphStyle.lineSpacing = paragraphStyle.lineSpacing
|
||||
newParagraphStyle.paragraphSpacing = paragraphStyle.paragraphSpacing
|
||||
newParagraphStyle.paragraphSpacingBefore = paragraphStyle.paragraphSpacingBefore
|
||||
newParagraphStyle.headIndent = 0
|
||||
newParagraphStyle.tailIndent = paragraphStyle.tailIndent
|
||||
newParagraphStyle.firstLineHeadIndent = 0
|
||||
newParagraphStyle.lineBreakMode = paragraphStyle.lineBreakMode
|
||||
newParagraphStyle.minimumLineHeight = paragraphStyle.minimumLineHeight
|
||||
newParagraphStyle.maximumLineHeight = paragraphStyle.maximumLineHeight
|
||||
newParagraphStyle.baseWritingDirection = paragraphStyle.baseWritingDirection
|
||||
newParagraphStyle.lineHeightMultiple = paragraphStyle.lineHeightMultiple
|
||||
newParagraphStyle.hyphenationFactor = paragraphStyle.hyphenationFactor
|
||||
newParagraphStyle.tabStops = paragraphStyle.tabStops
|
||||
newParagraphStyle.defaultTabInterval = paragraphStyle.defaultTabInterval
|
||||
newParagraphStyle.allowsDefaultTighteningForTruncation = paragraphStyle.allowsDefaultTighteningForTruncation
|
||||
|
||||
markerString.addAttribute(.paragraphStyle, value: newParagraphStyle, range: NSRange(location: 0, length: listMarker.count))
|
||||
newAttributedString.append(markerString)
|
||||
|
||||
let cleanedListItem = NSMutableAttributedString()
|
||||
listItemSubstring.enumerateAttributes(in: NSRange(location: 0, length: listItemSubstring.length), options: []) { itemAttributes, itemRange, _ in
|
||||
let itemSubstring = listItemSubstring.attributedSubstring(from: itemRange)
|
||||
let cleanedItemString = NSMutableAttributedString(attributedString: itemSubstring)
|
||||
|
||||
if let itemParagraphStyle = itemAttributes[.paragraphStyle] as? NSParagraphStyle,
|
||||
!itemParagraphStyle.textLists.isEmpty {
|
||||
cleanedItemString.addAttribute(.paragraphStyle, value: newParagraphStyle, range: NSRange(location: 0, length: cleanedItemString.length))
|
||||
}
|
||||
|
||||
cleanedListItem.append(cleanedItemString)
|
||||
}
|
||||
newAttributedString.append(cleanedListItem)
|
||||
result.append(newAttributedString)
|
||||
currentIndex = listItemRange.location + listItemRange.length
|
||||
} else {
|
||||
let charSubstring = attributedString.attributedSubstring(from: nsRange)
|
||||
result.append(charSubstring)
|
||||
currentIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func findListItemRange(in attributedString: NSAttributedString, startingAt index: Int) -> NSRange {
|
||||
let string = attributedString.string
|
||||
let startIndex = string.index(string.startIndex, offsetBy: index)
|
||||
|
||||
var endIndex = startIndex
|
||||
while endIndex < string.endIndex {
|
||||
let character = string[endIndex]
|
||||
if character == "\n" {
|
||||
endIndex = string.index(after: endIndex)
|
||||
break
|
||||
}
|
||||
endIndex = string.index(after: endIndex)
|
||||
}
|
||||
|
||||
let length = string.distance(from: startIndex, to: endIndex)
|
||||
return NSRange(location: index, length: length)
|
||||
}
|
||||
|
||||
private func generateListMarker(for paragraphStyle: NSParagraphStyle, counters: inout [NSTextList: Int]) -> String {
|
||||
guard let textList = paragraphStyle.textLists.first else { return "" }
|
||||
|
||||
if counters[textList] == nil {
|
||||
counters[textList] = 0
|
||||
}
|
||||
counters[textList]! += 1
|
||||
|
||||
let currentIndex = counters[textList]!
|
||||
let format = textList.markerFormat
|
||||
|
||||
let marker = generateMarkerText(format: format.rawValue, index: currentIndex)
|
||||
|
||||
return marker + " "
|
||||
}
|
||||
|
||||
private func generateMarkerText(format: String, index: Int) -> String {
|
||||
switch format {
|
||||
case "{decimal}":
|
||||
return "\(index)."
|
||||
case "{lower-alpha}":
|
||||
return "\(indexToLowerAlpha(index))."
|
||||
case "{upper-alpha}":
|
||||
return "\(indexToUpperAlpha(index))."
|
||||
case "{lower-roman}":
|
||||
return "\(indexToRoman(index))."
|
||||
case "{upper-roman}":
|
||||
return "\(indexToRoman(index).uppercased())."
|
||||
case "{disc}":
|
||||
return "•"
|
||||
case "{circle}":
|
||||
return "◦"
|
||||
case "{square}":
|
||||
return "▪"
|
||||
case "{hyphen}":
|
||||
return "-"
|
||||
case "{\"":
|
||||
return "-"
|
||||
default:
|
||||
if format.contains("decimal") {
|
||||
return "\(index)."
|
||||
} else if format.contains("alpha") {
|
||||
return "\(indexToLowerAlpha(index))."
|
||||
} else if format.contains("roman") {
|
||||
return "\(indexToRoman(index))."
|
||||
} else {
|
||||
return "•"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func indexToLowerAlpha(_ index: Int) -> String {
|
||||
let alphabet = "abcdefghijklmnopqrstuvwxyz"
|
||||
let alphabetArray = Array(alphabet)
|
||||
|
||||
if index <= 26 {
|
||||
return String(alphabetArray[index - 1])
|
||||
} else {
|
||||
let letterIndex = (index - 1) % 26
|
||||
let repeatCount = (index - 1) / 26 + 1
|
||||
return String(repeating: String(alphabetArray[letterIndex]), count: repeatCount)
|
||||
}
|
||||
}
|
||||
|
||||
private func indexToUpperAlpha(_ index: Int) -> String {
|
||||
return indexToLowerAlpha(index).uppercased()
|
||||
}
|
||||
|
||||
private func indexToRoman(_ index: Int) -> String {
|
||||
let romanNumerals = [
|
||||
(1000, "m"), (900, "cm"), (500, "d"), (400, "cd"),
|
||||
(100, "c"), (90, "xc"), (50, "l"), (40, "xl"),
|
||||
(10, "x"), (9, "ix"), (5, "v"), (4, "iv"), (1, "i")
|
||||
]
|
||||
var result = ""
|
||||
var number = index
|
||||
for (value, numeral) in romanNumerals {
|
||||
while number >= value {
|
||||
result += numeral
|
||||
number -= value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func chatInputStateString(attributedString: NSAttributedString) -> NSAttributedString? {
|
||||
let string = NSMutableAttributedString(string: attributedString.string)
|
||||
attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: [], using: { attributes, range, _ in
|
||||
let preprocessedString = preprocessLists(attributedString: attributedString)
|
||||
|
||||
let string = NSMutableAttributedString(string: preprocessedString.string)
|
||||
preprocessedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: [], using: { attributes, range, _ in
|
||||
if let value = attributes[.link], let url = (value as? URL)?.absoluteString {
|
||||
string.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: range)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -934,7 +934,7 @@ private struct PremiumProduct: Equatable {
|
|||
let storeProduct: InAppPurchaseManager.Product
|
||||
|
||||
var id: String {
|
||||
return self.storeProduct.id
|
||||
return self.option.storeProductId ?? self.storeProduct.id
|
||||
}
|
||||
|
||||
var months: Int32 {
|
||||
|
|
@ -1551,6 +1551,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
|
||||
}
|
||||
|
||||
var isBiannual: Bool {
|
||||
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".biannual") ?? false
|
||||
}
|
||||
|
||||
var canUpgrade: Bool {
|
||||
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
|
||||
if self.validPurchases.contains(where: { $0.transactionId == transactionId }) {
|
||||
|
|
@ -1970,6 +1974,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||
giftTitle = strings.Premium_Monthly
|
||||
} else if product.id.hasSuffix(".semiannual") {
|
||||
giftTitle = strings.Premium_Semiannual
|
||||
} else if product.id.hasSuffix(".biannual") {
|
||||
giftTitle = strings.Premium_Biannual
|
||||
} else {
|
||||
giftTitle = strings.Premium_Annual
|
||||
}
|
||||
|
|
@ -1994,7 +2000,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||
if discountValue > 0 {
|
||||
subtitle = "**\(defaultPrice)** \(product.price)"
|
||||
accessibilitySubtitle = product.price
|
||||
if product.months == 12 {
|
||||
if product.months == 24 {
|
||||
subtitle = environment.strings.Premium_PricePer2Years(subtitle).string
|
||||
accessibilitySubtitle = environment.strings.Premium_PricePer2Years(accessibilitySubtitle).string
|
||||
} else if product.months == 12 {
|
||||
subtitle = environment.strings.Premium_PricePerYear(subtitle).string
|
||||
accessibilitySubtitle = environment.strings.Premium_PricePerYear(accessibilitySubtitle).string
|
||||
}
|
||||
|
|
@ -2174,8 +2183,21 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||
}
|
||||
|
||||
let isPremium = state?.isPremium == true
|
||||
let buttonText: String
|
||||
if isPremium {
|
||||
buttonText = strings.Common_OK
|
||||
} else {
|
||||
if state?.isAnnual == true {
|
||||
buttonText = strings.Premium_SubscribeForAnnual(state?.price ?? "—").string
|
||||
} else if state?.isBiannual == true {
|
||||
buttonText = strings.Premium_SubscribeForBiannual(state?.price ?? "—").string
|
||||
} else {
|
||||
buttonText = strings.Premium_SubscribeFor(state?.price ?? "–").string
|
||||
}
|
||||
}
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark)
|
||||
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: buttonText, isPremium: isPremium, forceDark: forceDark)
|
||||
controller.action = { [weak state] in
|
||||
dismissImpl?()
|
||||
if state?.isPremium == false {
|
||||
|
|
@ -2404,8 +2426,23 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||
default:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
||||
let buttonText: String
|
||||
if isPremium {
|
||||
buttonText = strings.Common_OK
|
||||
} else {
|
||||
if state?.isAnnual == true {
|
||||
buttonText = strings.Premium_SubscribeForAnnual(state?.price ?? "—").string
|
||||
} else if state?.isBiannual == true {
|
||||
buttonText = strings.Premium_SubscribeForBiannual(state?.price ?? "—").string
|
||||
} else {
|
||||
buttonText = strings.Premium_SubscribeFor(state?.price ?? "–").string
|
||||
}
|
||||
}
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.businessPerks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark)
|
||||
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.businessPerks, buttonText: buttonText, isPremium: isPremium, forceDark: forceDark)
|
||||
controller.action = { [weak state] in
|
||||
dismissImpl?()
|
||||
if state?.isPremium == false {
|
||||
|
|
@ -2965,6 +3002,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".annual") ?? false
|
||||
}
|
||||
|
||||
var isBiannual: Bool {
|
||||
return self.products?.first(where: { $0.id == self.selectedProductId })?.id.hasSuffix(".biannual") ?? false
|
||||
}
|
||||
|
||||
var canUpgrade: Bool {
|
||||
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
|
||||
if self.validPurchases.contains(where: { $0.transactionId == transactionId }) {
|
||||
|
|
@ -3056,6 +3097,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||
}
|
||||
}
|
||||
|
||||
//TODO:release
|
||||
if let product = availableProducts.first(where: { $0.id.hasSuffix(".annual") }) {
|
||||
let (currency, price) = product.priceCurrencyAndAmount
|
||||
products.insert(PremiumProduct(option: PremiumPromoConfiguration.PremiumProductOption(isCurrent: false, months: 24, currency: currency, amount: price * 2, botUrl: "", transactionId: nil, availableForUpgrade: true, storeProductId: "org.telegram.telegramPremium.biannual"), storeProduct: product), at: 0)
|
||||
}
|
||||
|
||||
strongSelf.products = products
|
||||
strongSelf.isPremium = forceHasPremium || isPremium
|
||||
strongSelf.otherPeerName = otherPeerName
|
||||
|
|
@ -3719,7 +3766,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||
} else if state.isPremium == true && state.canUpgrade {
|
||||
buttonTitle = state.isAnnual ? environment.strings.Premium_UpgradeForAnnual(state.price ?? "—").string : environment.strings.Premium_UpgradeFor(state.price ?? "—").string
|
||||
} else {
|
||||
buttonTitle = state.isAnnual ? environment.strings.Premium_SubscribeForAnnual(state.price ?? "—").string : environment.strings.Premium_SubscribeFor(state.price ?? "—").string
|
||||
if state.isAnnual {
|
||||
buttonTitle = environment.strings.Premium_SubscribeForAnnual(state.price ?? "—").string
|
||||
} else if state.isBiannual {
|
||||
buttonTitle = environment.strings.Premium_SubscribeForBiannual(state.price ?? "—").string
|
||||
} else {
|
||||
buttonTitle = environment.strings.Premium_SubscribeFor(state.price ?? "–").string
|
||||
}
|
||||
}
|
||||
|
||||
let controller = environment.controller
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/GenerateThemeName",
|
||||
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
|
||||
"//submodules/TelegramUI/Components/FaceScanScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import OpenInExternalAppUI
|
|||
import ItemListPeerActionItem
|
||||
import StorageUsageScreen
|
||||
import PresentationDataUtils
|
||||
import FaceScanScreen
|
||||
|
||||
public enum AutomaticSaveIncomingPeerType {
|
||||
case privateChats
|
||||
|
|
@ -617,7 +618,7 @@ private func autosaveLabelAndValue(presentationData: PresentationData, settings:
|
|||
return (label, value)
|
||||
}
|
||||
|
||||
private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, defaultWebBrowser: String, contentSettingsConfiguration: ContentSettingsConfiguration?, networkUsage: Int64, storageUsage: Int64, mediaAutoSaveSettings: MediaAutoSaveSettings, autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?], mediaSettings: MediaDisplaySettings) -> [DataAndStorageEntry] {
|
||||
private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData, defaultWebBrowser: String, contentSettingsConfiguration: ContentSettingsConfiguration?, networkUsage: Int64, storageUsage: Int64, mediaAutoSaveSettings: MediaAutoSaveSettings, autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?], mediaSettings: MediaDisplaySettings, showSensitiveContentSetting: Bool) -> [DataAndStorageEntry] {
|
||||
var entries: [DataAndStorageEntry] = []
|
||||
|
||||
entries.append(.storageUsage(presentationData.theme, presentationData.strings.ChatSettings_Cache, dataSizeString(storageUsage, formatting: DataSizeStringFormatting(presentationData: presentationData))))
|
||||
|
|
@ -656,7 +657,7 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
|
|||
entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak))
|
||||
entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo))
|
||||
|
||||
if !"".isEmpty, let contentSettingsConfiguration = contentSettingsConfiguration, contentSettingsConfiguration.canAdjustSensitiveContent {
|
||||
if let contentSettingsConfiguration = contentSettingsConfiguration, contentSettingsConfiguration.canAdjustSensitiveContent && showSensitiveContentSetting {
|
||||
entries.append(.sensitiveContent(presentationData.strings.Settings_SensitiveContent, contentSettingsConfiguration.sensitiveContentEnabled))
|
||||
entries.append(.sensitiveContentInfo(presentationData.strings.Settings_SensitiveContentInfo))
|
||||
}
|
||||
|
|
@ -911,6 +912,12 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
|
|||
}
|
||||
})
|
||||
updateSensitiveContentDisposable.set(updateRemoteContentSettingsConfiguration(postbox: context.account.postbox, network: context.account.network, sensitiveContentEnabled: value).start())
|
||||
|
||||
if !value {
|
||||
let _ = updateAgeVerificationState(engine: context.engine, { _ in
|
||||
return AgeVerificationState(verificationPassed: false)
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
if value {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
|
@ -944,6 +951,7 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
|
|||
}
|
||||
|
||||
let sensitiveContent = Atomic<Bool?>(value: nil)
|
||||
let canAdjustSensitiveContent = Atomic<Bool?>(value: nil)
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
context.sharedContext.presentationData,
|
||||
|
|
@ -974,9 +982,14 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
|
|||
if previousSensitiveContent != contentSettingsConfiguration?.sensitiveContentEnabled {
|
||||
animateChanges = true
|
||||
}
|
||||
|
||||
if canAdjustSensitiveContent.with({ $0 }) == nil {
|
||||
let _ = canAdjustSensitiveContent.swap(contentSettingsConfiguration?.sensitiveContentEnabled)
|
||||
}
|
||||
let showSensitiveContentSetting = canAdjustSensitiveContent.with { $0 } ?? false
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataAndStorageControllerEntries(state: state, data: dataAndStorageData, presentationData: presentationData, defaultWebBrowser: defaultWebBrowser, contentSettingsConfiguration: contentSettingsConfiguration, networkUsage: usageSignal.network, storageUsage: usageSignal.storage, mediaAutoSaveSettings: mediaAutoSaveSettings, autosaveExceptionPeers: autosaveExceptionPeers, mediaSettings: mediaSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: animateChanges)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataAndStorageControllerEntries(state: state, data: dataAndStorageData, presentationData: presentationData, defaultWebBrowser: defaultWebBrowser, contentSettingsConfiguration: contentSettingsConfiguration, networkUsage: usageSignal.network, storageUsage: usageSignal.storage, mediaAutoSaveSettings: mediaAutoSaveSettings, autosaveExceptionPeers: autosaveExceptionPeers, mediaSettings: mediaSettings, showSensitiveContentSetting: showSensitiveContentSetting), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: animateChanges)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
|
|
|
|||
|
|
@ -437,10 +437,15 @@ public final class ShareController: ViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var openShareAsImage: (([Message]) -> Void)?
|
||||
|
||||
|
||||
public var shareStory: (() -> Void)?
|
||||
public var canSendInHighQuality = false {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.controllerNode.canSendInHighQuality = self.canSendInHighQuality
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var debugAction: (() -> Void)?
|
||||
|
||||
|
|
@ -692,14 +697,46 @@ public final class ShareController: ViewController {
|
|||
mediaParameters = parameters
|
||||
}
|
||||
|
||||
self.displayNode = ShareControllerNode(controller: self, environment: self.environment, presentationData: self.presentationData, presetText: self.presetText, defaultAction: self.defaultAction, mediaParameters: mediaParameters, requestLayout: { [weak self] transition in
|
||||
self?.requestLayout(transition: transition)
|
||||
}, presentError: { [weak self] title, text in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory, collectibleItemInfo: self.collectibleItemInfo, messageCount: messageCount)
|
||||
self.displayNode = ShareControllerNode(
|
||||
controller: self,
|
||||
environment: self.environment,
|
||||
presentationData: self.presentationData,
|
||||
presetText: self.presetText,
|
||||
defaultAction: self.defaultAction,
|
||||
mediaParameters: mediaParameters,
|
||||
requestLayout: { [weak self] transition in
|
||||
self?.requestLayout(
|
||||
transition: transition
|
||||
)
|
||||
},
|
||||
presentError: { [weak self] title, text in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(
|
||||
theme: AlertControllerTheme(presentationData: strongSelf.presentationData),
|
||||
title: title,
|
||||
text: text,
|
||||
actions: [TextAlertAction(
|
||||
type: .defaultAction,
|
||||
title: strongSelf.presentationData.strings.Common_OK,
|
||||
action: {
|
||||
})
|
||||
]
|
||||
), in: .window(.root))
|
||||
},
|
||||
externalShare: self.externalShare,
|
||||
immediateExternalShare: self.immediateExternalShare,
|
||||
immediatePeerId: self.immediatePeerId,
|
||||
fromForeignApp: self.fromForeignApp,
|
||||
forceTheme: self.forceTheme,
|
||||
fromPublicChannel: fromPublicChannel,
|
||||
segmentedValues: self.segmentedValues,
|
||||
shareStory: self.shareStory,
|
||||
collectibleItemInfo: self.collectibleItemInfo,
|
||||
messageCount: messageCount
|
||||
)
|
||||
self.controllerNode.canSendInHighQuality = self.canSendInHighQuality
|
||||
self.controllerNode.completed = self.completed
|
||||
self.controllerNode.enqueued = self.enqueued
|
||||
self.controllerNode.present = { [weak self] c in
|
||||
|
|
|
|||
|
|
@ -329,6 +329,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
private let collectibleItemInfo: TelegramCollectibleItemInfo?
|
||||
private let mediaParameters: ShareControllerSubject.MediaParameters?
|
||||
private let messageCount: Int
|
||||
var canSendInHighQuality = false
|
||||
|
||||
var selectedSegmentedIndex: Int = 0
|
||||
|
||||
|
|
@ -389,7 +390,26 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
|
||||
private let showNames = ValuePromise<Bool>(true)
|
||||
|
||||
init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, mediaParameters: ShareControllerSubject.MediaParameters?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?, messageCount: Int) {
|
||||
init(
|
||||
controller: ShareController,
|
||||
environment: ShareControllerEnvironment,
|
||||
presentationData: PresentationData,
|
||||
presetText: String?,
|
||||
defaultAction: ShareControllerAction?,
|
||||
mediaParameters: ShareControllerSubject.MediaParameters?,
|
||||
requestLayout: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
presentError: @escaping (String?, String) -> Void,
|
||||
externalShare: Bool,
|
||||
immediateExternalShare: Bool,
|
||||
immediatePeerId: PeerId?,
|
||||
fromForeignApp: Bool,
|
||||
forceTheme: PresentationTheme?,
|
||||
fromPublicChannel: Bool,
|
||||
segmentedValues: [ShareControllerSegmentedValue]?,
|
||||
shareStory: (() -> Void)?,
|
||||
collectibleItemInfo: TelegramCollectibleItemInfo?,
|
||||
messageCount: Int
|
||||
) {
|
||||
self.controller = controller
|
||||
self.environment = environment
|
||||
self.presentationData = presentationData
|
||||
|
|
@ -544,6 +564,18 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
|
|||
.separator,
|
||||
])
|
||||
}
|
||||
|
||||
if fromForeignApp, strongSelf.canSendInHighQuality {
|
||||
items.append(
|
||||
.action(ContextMenuActionItem(text: presentationData.strings.Attachment_SendInHd, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QualityHd"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
f(.default)
|
||||
if let strongSelf = self {
|
||||
strongSelf.send(showNames: showNamesValue, silently: true)
|
||||
}
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
items.append(contentsOf: [
|
||||
.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
|
|
|||
|
|
@ -1251,7 +1251,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||
|
||||
if self.isReordering, let contentItem, contentItem.isReorderable {
|
||||
if layer.animation(forKey: "shaking_position") == nil {
|
||||
startShaking(layer: layer)
|
||||
layer.addReorderingShaking()
|
||||
}
|
||||
} else {
|
||||
if layer.animation(forKey: "shaking_position") != nil {
|
||||
|
|
@ -2327,51 +2327,6 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
private func startShaking(layer: CALayer) {
|
||||
func degreesToRadians(_ x: CGFloat) -> CGFloat {
|
||||
return .pi * x / 180.0
|
||||
}
|
||||
|
||||
let duration: Double = 0.4
|
||||
let displacement: CGFloat = 1.0
|
||||
let degreesRotation: CGFloat = 2.0
|
||||
|
||||
let negativeDisplacement = -1.0 * displacement
|
||||
let position = CAKeyframeAnimation.init(keyPath: "position")
|
||||
position.beginTime = 0.8
|
||||
position.duration = duration
|
||||
position.values = [
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
|
||||
NSValue(cgPoint: CGPoint(x: 0, y: 0)),
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
|
||||
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
|
||||
]
|
||||
position.calculationMode = .linear
|
||||
position.isRemovedOnCompletion = false
|
||||
position.repeatCount = Float.greatestFiniteMagnitude
|
||||
position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
||||
position.isAdditive = true
|
||||
|
||||
let transform = CAKeyframeAnimation.init(keyPath: "transform")
|
||||
transform.beginTime = 2.6
|
||||
transform.duration = 0.3
|
||||
transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
|
||||
transform.values = [
|
||||
degreesToRadians(-1.0 * degreesRotation),
|
||||
degreesToRadians(degreesRotation),
|
||||
degreesToRadians(-1.0 * degreesRotation)
|
||||
]
|
||||
transform.calculationMode = .linear
|
||||
transform.isRemovedOnCompletion = false
|
||||
transform.repeatCount = Float.greatestFiniteMagnitude
|
||||
transform.isAdditive = true
|
||||
transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
||||
|
||||
layer.add(position, forKey: "shaking_position")
|
||||
layer.add(transform, forKey: "shaking_rotation")
|
||||
}
|
||||
|
||||
private final class ReorderGestureRecognizer: UIGestureRecognizer {
|
||||
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SparseItemGridDisplayItem?)
|
||||
private let willBegin: (CGPoint) -> Void
|
||||
|
|
|
|||
|
|
@ -880,7 +880,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
|||
dict[1681948327] = { return Api.SavedDialog.parse_monoForumDialog($0) }
|
||||
dict[-1115174036] = { return Api.SavedDialog.parse_savedDialog($0) }
|
||||
dict[-881854424] = { return Api.SavedReactionTag.parse_savedReactionTag($0) }
|
||||
dict[-539360103] = { return Api.SavedStarGift.parse_savedStarGift($0) }
|
||||
dict[514213599] = { return Api.SavedStarGift.parse_savedStarGift($0) }
|
||||
dict[-911191137] = { return Api.SearchResultsCalendarPeriod.parse_searchResultsCalendarPeriod($0) }
|
||||
dict[2137295719] = { return Api.SearchResultsPosition.parse_searchResultPosition($0) }
|
||||
dict[871426631] = { return Api.SecureCredentialsEncrypted.parse_secureCredentialsEncrypted($0) }
|
||||
|
|
|
|||
|
|
@ -196,13 +196,13 @@ public extension Api {
|
|||
}
|
||||
public extension Api {
|
||||
enum SavedStarGift: TypeConstructorDescription {
|
||||
case savedStarGift(flags: Int32, fromId: Api.Peer?, date: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, msgId: Int32?, savedId: Int64?, convertStars: Int64?, upgradeStars: Int64?, canExportAt: Int32?, transferStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?)
|
||||
case savedStarGift(flags: Int32, fromId: Api.Peer?, date: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, msgId: Int32?, savedId: Int64?, convertStars: Int64?, upgradeStars: Int64?, canExportAt: Int32?, transferStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?, collectionId: [Int32]?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt):
|
||||
case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId):
|
||||
if boxed {
|
||||
buffer.appendInt32(-539360103)
|
||||
buffer.appendInt32(514213599)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 1) != 0 {fromId!.serialize(buffer, true)}
|
||||
|
|
@ -217,14 +217,19 @@ public extension Api {
|
|||
if Int(flags) & Int(1 << 8) != 0 {serializeInt64(transferStars!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 13) != 0 {serializeInt32(canTransferAt!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 14) != 0 {serializeInt32(canResellAt!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 15) != 0 {buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(collectionId!.count))
|
||||
for item in collectionId! {
|
||||
serializeInt32(item, buffer: buffer, boxed: false)
|
||||
}}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt):
|
||||
return ("savedStarGift", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("gift", gift as Any), ("message", message as Any), ("msgId", msgId as Any), ("savedId", savedId as Any), ("convertStars", convertStars as Any), ("upgradeStars", upgradeStars as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any)])
|
||||
case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId):
|
||||
return ("savedStarGift", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("gift", gift as Any), ("message", message as Any), ("msgId", msgId as Any), ("savedId", savedId as Any), ("convertStars", convertStars as Any), ("upgradeStars", upgradeStars as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any), ("collectionId", collectionId as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -261,6 +266,10 @@ public extension Api {
|
|||
if Int(_1!) & Int(1 << 13) != 0 {_12 = reader.readInt32() }
|
||||
var _13: Int32?
|
||||
if Int(_1!) & Int(1 << 14) != 0 {_13 = reader.readInt32() }
|
||||
var _14: [Int32]?
|
||||
if Int(_1!) & Int(1 << 15) != 0 {if let _ = reader.readInt32() {
|
||||
_14 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
|
|
@ -274,8 +283,9 @@ public extension Api {
|
|||
let _c11 = (Int(_1!) & Int(1 << 8) == 0) || _11 != nil
|
||||
let _c12 = (Int(_1!) & Int(1 << 13) == 0) || _12 != nil
|
||||
let _c13 = (Int(_1!) & Int(1 << 14) == 0) || _13 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 {
|
||||
return Api.SavedStarGift.savedStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, savedId: _7, convertStars: _8, upgradeStars: _9, canExportAt: _10, transferStars: _11, canTransferAt: _12, canResellAt: _13)
|
||||
let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 {
|
||||
return Api.SavedStarGift.savedStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, savedId: _7, convertStars: _8, upgradeStars: _9, canExportAt: _10, transferStars: _11, canTransferAt: _12, canResellAt: _13, collectionId: _14)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1087,7 +1087,8 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
|
|||
upgradeStars: nil,
|
||||
transferStars: transferStars,
|
||||
canTransferDate: canTransferDate,
|
||||
canResaleDate: canResaleDate
|
||||
canResaleDate: canResaleDate,
|
||||
collectionIds: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -1728,6 +1729,16 @@ private final class ProfileGiftsContextImpl {
|
|||
}
|
||||
self.gifts = updatedGifts
|
||||
self.pushState()
|
||||
|
||||
let updatedCount = self.count ?? 0
|
||||
|
||||
let peerId = self.peerId
|
||||
let collectionId = self.collectionId
|
||||
self.cacheDisposable.set(self.account.postbox.transaction { transaction in
|
||||
if let entry = CodableEntry(CachedProfileGifts(gifts: updatedGifts, count: updatedCount, notificationsEnabled: nil)) {
|
||||
transaction.putItemCacheEntry(id: giftsEntryId(peerId: peerId, collectionId: collectionId), entry: entry)
|
||||
}
|
||||
}.start())
|
||||
}
|
||||
|
||||
func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal<ProfileGiftsContext.State.StarGift, UpgradeStarGiftError> {
|
||||
|
|
@ -1929,6 +1940,7 @@ public final class ProfileGiftsContext {
|
|||
case giftAddress
|
||||
case canTransferDate
|
||||
case canResaleDate
|
||||
case collectionIds
|
||||
}
|
||||
|
||||
public let gift: TelegramCore.StarGift
|
||||
|
|
@ -1947,7 +1959,8 @@ public final class ProfileGiftsContext {
|
|||
public let transferStars: Int64?
|
||||
public let canTransferDate: Int32?
|
||||
public let canResaleDate: Int32?
|
||||
|
||||
public let collectionIds: [Int32]?
|
||||
|
||||
fileprivate let _fromPeerId: EnginePeer.Id?
|
||||
|
||||
public enum DecodingError: Error {
|
||||
|
|
@ -1970,7 +1983,8 @@ public final class ProfileGiftsContext {
|
|||
upgradeStars: Int64?,
|
||||
transferStars: Int64?,
|
||||
canTransferDate: Int32?,
|
||||
canResaleDate: Int32?
|
||||
canResaleDate: Int32?,
|
||||
collectionIds: [Int32]?
|
||||
) {
|
||||
self.gift = gift
|
||||
self.reference = reference
|
||||
|
|
@ -1989,6 +2003,7 @@ public final class ProfileGiftsContext {
|
|||
self.transferStars = transferStars
|
||||
self.canTransferDate = canTransferDate
|
||||
self.canResaleDate = canResaleDate
|
||||
self.collectionIds = collectionIds
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -2017,6 +2032,7 @@ public final class ProfileGiftsContext {
|
|||
self.transferStars = try container.decodeIfPresent(Int64.self, forKey: .transferStars)
|
||||
self.canTransferDate = try container.decodeIfPresent(Int32.self, forKey: .canTransferDate)
|
||||
self.canResaleDate = try container.decodeIfPresent(Int32.self, forKey: .canResaleDate)
|
||||
self.collectionIds = try container.decodeIfPresent([Int32].self, forKey: .collectionIds)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
|
@ -2038,6 +2054,7 @@ public final class ProfileGiftsContext {
|
|||
try container.encodeIfPresent(self.transferStars, forKey: .transferStars)
|
||||
try container.encodeIfPresent(self.canTransferDate, forKey: .canTransferDate)
|
||||
try container.encodeIfPresent(self.canResaleDate, forKey: .canResaleDate)
|
||||
try container.encodeIfPresent(self.collectionIds, forKey: .collectionIds)
|
||||
}
|
||||
|
||||
public func withGift(_ gift: TelegramCore.StarGift) -> StarGift {
|
||||
|
|
@ -2057,7 +2074,8 @@ public final class ProfileGiftsContext {
|
|||
upgradeStars: self.upgradeStars,
|
||||
transferStars: self.transferStars,
|
||||
canTransferDate: self.canTransferDate,
|
||||
canResaleDate: self.canResaleDate
|
||||
canResaleDate: self.canResaleDate,
|
||||
collectionIds: self.collectionIds
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -2078,7 +2096,8 @@ public final class ProfileGiftsContext {
|
|||
upgradeStars: self.upgradeStars,
|
||||
transferStars: self.transferStars,
|
||||
canTransferDate: self.canTransferDate,
|
||||
canResaleDate: self.canResaleDate
|
||||
canResaleDate: self.canResaleDate,
|
||||
collectionIds: self.collectionIds
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -2099,7 +2118,8 @@ public final class ProfileGiftsContext {
|
|||
upgradeStars: self.upgradeStars,
|
||||
transferStars: self.transferStars,
|
||||
canTransferDate: self.canTransferDate,
|
||||
canResaleDate: self.canResaleDate
|
||||
canResaleDate: self.canResaleDate,
|
||||
collectionIds: self.collectionIds
|
||||
)
|
||||
}
|
||||
fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift {
|
||||
|
|
@ -2119,7 +2139,8 @@ public final class ProfileGiftsContext {
|
|||
upgradeStars: self.upgradeStars,
|
||||
transferStars: self.transferStars,
|
||||
canTransferDate: self.canTransferDate,
|
||||
canResaleDate: self.canResaleDate
|
||||
canResaleDate: self.canResaleDate,
|
||||
collectionIds: self.collectionIds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2323,7 +2344,7 @@ public final class ProfileGiftsContext {
|
|||
extension ProfileGiftsContext.State.StarGift {
|
||||
init?(apiSavedStarGift: Api.SavedStarGift, peerId: EnginePeer.Id, transaction: Transaction) {
|
||||
switch apiSavedStarGift {
|
||||
case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt):
|
||||
case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt, collectionIds):
|
||||
guard let gift = StarGift(apiStarGift: apiGift) else {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -2369,6 +2390,7 @@ extension ProfileGiftsContext.State.StarGift {
|
|||
self.transferStars = transferStars
|
||||
self.canTransferDate = canTransferAt
|
||||
self.canResaleDate = canResaleAt
|
||||
self.collectionIds = collectionIds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,8 +98,8 @@ private func _internal_getStarGiftCollections(postbox: Postbox, network: Network
|
|||
let collections = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedProfileGiftsCollections.self)
|
||||
return (inputPeer, collections?.collections)
|
||||
}
|
||||
|> mapToSignal { inputPeerAndHash -> Signal<[StarGiftCollection]?, NoError> in
|
||||
guard let (inputPeer, cachedCollections) = inputPeerAndHash else {
|
||||
|> mapToSignal { inputPeerAndCollections -> Signal<[StarGiftCollection]?, NoError> in
|
||||
guard let (inputPeer, cachedCollections) = inputPeerAndCollections else {
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
|
|
@ -108,28 +108,28 @@ private func _internal_getStarGiftCollections(postbox: Postbox, network: Network
|
|||
hash = intListSimpleHash(cachedCollections.map { $0.hash })
|
||||
}
|
||||
|
||||
return network.request(Api.functions.payments.getStarGiftCollections(peer: inputPeer, hash: hash))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.payments.StarGiftCollections?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<[StarGiftCollection]?, NoError> in
|
||||
guard let result else {
|
||||
return .single(cachedCollections)
|
||||
|> then(
|
||||
network.request(Api.functions.payments.getStarGiftCollections(peer: inputPeer, hash: hash))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.payments.StarGiftCollections?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
return postbox.transaction { transaction -> [StarGiftCollection]? in
|
||||
switch result {
|
||||
case let .starGiftCollections(collections):
|
||||
let collections = collections.compactMap { StarGiftCollection(apiStarGiftCollection: $0) }
|
||||
if let entry = CodableEntry(CachedProfileGiftsCollections(collections: collections)) {
|
||||
transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry)
|
||||
|> mapToSignal { result -> Signal<[StarGiftCollection]?, NoError> in
|
||||
guard let result else {
|
||||
return .single(nil)
|
||||
}
|
||||
return postbox.transaction { transaction -> [StarGiftCollection]? in
|
||||
switch result {
|
||||
case let .starGiftCollections(collections):
|
||||
let collections = collections.compactMap { StarGiftCollection(apiStarGiftCollection: $0) }
|
||||
return collections
|
||||
case .starGiftCollectionsNotModified:
|
||||
return cachedCollections ?? []
|
||||
}
|
||||
return collections
|
||||
case .starGiftCollectionsNotModified:
|
||||
return cachedCollections ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -341,6 +341,7 @@ public final class ProfileGiftsCollectionsContext {
|
|||
self.collections = collections ?? []
|
||||
self.isLoading = false
|
||||
self.pushState()
|
||||
self.updateCache()
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -354,6 +355,7 @@ public final class ProfileGiftsCollectionsContext {
|
|||
if let collection {
|
||||
self.collections.append(collection)
|
||||
self.pushState()
|
||||
self.updateCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -370,6 +372,7 @@ public final class ProfileGiftsCollectionsContext {
|
|||
if let index = self.collections.firstIndex(where: { $0.id == id }) {
|
||||
self.collections[index] = collection
|
||||
self.pushState()
|
||||
self.updateCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -392,7 +395,8 @@ public final class ProfileGiftsCollectionsContext {
|
|||
}
|
||||
|
||||
public func reorderCollections(order: [Int32]) -> Signal<Bool, NoError> {
|
||||
return _internal_reorderStarGiftCollections(account: self.account, peerId: self.peerId, order: order)
|
||||
let peerId = self.peerId
|
||||
return _internal_reorderStarGiftCollections(account: self.account, peerId: peerId, order: order)
|
||||
|> deliverOn(self.queue)
|
||||
|> afterNext { [weak self] collection in
|
||||
guard let self else {
|
||||
|
|
@ -410,6 +414,7 @@ public final class ProfileGiftsCollectionsContext {
|
|||
}
|
||||
self.collections = collections
|
||||
self.pushState()
|
||||
self.updateCache()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -423,9 +428,20 @@ public final class ProfileGiftsCollectionsContext {
|
|||
self.giftsContexts.removeValue(forKey: id)
|
||||
self.collections.removeAll(where: { $0.id == id })
|
||||
self.pushState()
|
||||
self.updateCache()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCache() {
|
||||
let peerId = self.peerId
|
||||
let collections = self.collections
|
||||
let _ = (self.account.postbox.transaction { transaction in
|
||||
if let entry = CodableEntry(CachedProfileGiftsCollections(collections: collections)) {
|
||||
transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry)
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
private func pushState() {
|
||||
let state = State(
|
||||
collections: self.collections,
|
||||
|
|
|
|||
|
|
@ -1644,7 +1644,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
|
|||
upgradeStars: nil,
|
||||
transferStars: transferStars,
|
||||
canTransferDate: canTransferDate,
|
||||
canResaleDate: canResaleDate
|
||||
canResaleDate: canResaleDate,
|
||||
collectionIds: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -482,6 +482,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/ComposeTodoScreen",
|
||||
"//submodules/TelegramUI/Components/SuggestedPostApproveAlert",
|
||||
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
|
||||
"//submodules/TelegramUI/Components/FaceScanScreen",
|
||||
"//submodules/ContactsHelper",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ public final class ButtonComponent: Component {
|
|||
public init(
|
||||
background: Background,
|
||||
content: AnyComponentWithIdentity<Empty>,
|
||||
isEnabled: Bool,
|
||||
isEnabled: Bool = true,
|
||||
tintWhenDisabled: Bool = true,
|
||||
allowActionWhenDisabled: Bool = false,
|
||||
displaysProgress: Bool = false,
|
||||
|
|
|
|||
|
|
@ -2992,7 +2992,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||
|
||||
var viewText: String = ""
|
||||
if case .eye = icon {
|
||||
viewText = strings.Chat_SensitiveContent
|
||||
viewText = wideLayout ? strings.Chat_SensitiveContent : strings.Chat_SensitiveContentShort
|
||||
extendedMediaOverlayNode.dustNode.revealOnTap = false
|
||||
} else {
|
||||
outer: for attribute in message.attributes {
|
||||
|
|
|
|||
41
submodules/TelegramUI/Components/FaceScanScreen/BUILD
Normal file
41
submodules/TelegramUI/Components/FaceScanScreen/BUILD
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "FaceScanScreen",
|
||||
module_name = "FaceScanScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/Camera",
|
||||
"//submodules/FileMediaResourceStatus",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/DeviceAccess",
|
||||
"//third-party/ZipArchive",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
//
|
||||
// AgeNet.swift
|
||||
//
|
||||
// This file was automatically generated and should not be edited.
|
||||
//
|
||||
|
||||
import CoreML
|
||||
|
||||
|
||||
/// Model Prediction Input Type
|
||||
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
|
||||
class AgeNetInput : MLFeatureProvider {
|
||||
|
||||
/// input as color (kCVPixelFormatType_32BGRA) image buffer, 112 pixels wide by 112 pixels high
|
||||
var input: CVPixelBuffer
|
||||
|
||||
var featureNames: Set<String> { ["input"] }
|
||||
|
||||
func featureValue(for featureName: String) -> MLFeatureValue? {
|
||||
if featureName == "input" {
|
||||
return MLFeatureValue(pixelBuffer: input)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
init(input: CVPixelBuffer) {
|
||||
self.input = input
|
||||
}
|
||||
|
||||
convenience init(inputWith input: CGImage) throws {
|
||||
self.init(input: try MLFeatureValue(cgImage: input, pixelsWide: 112, pixelsHigh: 112, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!)
|
||||
}
|
||||
|
||||
convenience init(inputAt input: URL) throws {
|
||||
self.init(input: try MLFeatureValue(imageAt: input, pixelsWide: 112, pixelsHigh: 112, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!)
|
||||
}
|
||||
|
||||
func setInput(with input: CGImage) throws {
|
||||
self.input = try MLFeatureValue(cgImage: input, pixelsWide: 112, pixelsHigh: 112, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!
|
||||
}
|
||||
|
||||
func setInput(with input: URL) throws {
|
||||
self.input = try MLFeatureValue(imageAt: input, pixelsWide: 112, pixelsHigh: 112, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Model Prediction Output Type
|
||||
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
|
||||
class AgeNetOutput : MLFeatureProvider {
|
||||
|
||||
/// Source provided by CoreML
|
||||
private let provider : MLFeatureProvider
|
||||
|
||||
/// Identity as 1 by 1 matrix of floats
|
||||
var Identity: MLMultiArray {
|
||||
provider.featureValue(for: "Identity")!.multiArrayValue!
|
||||
}
|
||||
|
||||
/// Identity as 1 by 1 matrix of floats
|
||||
var IdentityShapedArray: MLShapedArray<Float> {
|
||||
MLShapedArray<Float>(Identity)
|
||||
}
|
||||
|
||||
var featureNames: Set<String> {
|
||||
provider.featureNames
|
||||
}
|
||||
|
||||
func featureValue(for featureName: String) -> MLFeatureValue? {
|
||||
provider.featureValue(for: featureName)
|
||||
}
|
||||
|
||||
init(Identity: MLMultiArray) {
|
||||
self.provider = try! MLDictionaryFeatureProvider(dictionary: ["Identity" : MLFeatureValue(multiArray: Identity)])
|
||||
}
|
||||
|
||||
init(features: MLFeatureProvider) {
|
||||
self.provider = features
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Class for model loading and prediction
|
||||
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
|
||||
class AgeNet {
|
||||
let model: MLModel
|
||||
|
||||
/// URL of model assuming it was installed in the same bundle as this class
|
||||
class var urlOfModelInThisBundle : URL {
|
||||
let bundle = Bundle(for: self)
|
||||
return bundle.url(forResource: "AgeNet", withExtension:"mlmodelc")!
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNet instance with an existing MLModel object.
|
||||
|
||||
Usually the application does not use this initializer unless it makes a subclass of AgeNet.
|
||||
Such application may want to use `MLModel(contentsOfURL:configuration:)` and `AgeNet.urlOfModelInThisBundle` to create a MLModel object to pass-in.
|
||||
|
||||
- parameters:
|
||||
- model: MLModel object
|
||||
*/
|
||||
init(model: MLModel) {
|
||||
self.model = model
|
||||
}
|
||||
|
||||
/**
|
||||
Construct a model with configuration
|
||||
|
||||
- parameters:
|
||||
- configuration: the desired model configuration
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
*/
|
||||
convenience init(configuration: MLModelConfiguration = MLModelConfiguration()) throws {
|
||||
try self.init(contentsOf: type(of:self).urlOfModelInThisBundle, configuration: configuration)
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNet instance with explicit path to mlmodelc file
|
||||
- parameters:
|
||||
- modelURL: the file url of the model
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
*/
|
||||
convenience init(contentsOf modelURL: URL) throws {
|
||||
try self.init(model: MLModel(contentsOf: modelURL))
|
||||
}
|
||||
|
||||
/**
|
||||
Construct a model with URL of the .mlmodelc directory and configuration
|
||||
|
||||
- parameters:
|
||||
- modelURL: the file url of the model
|
||||
- configuration: the desired model configuration
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
*/
|
||||
convenience init(contentsOf modelURL: URL, configuration: MLModelConfiguration) throws {
|
||||
try self.init(model: MLModel(contentsOf: modelURL, configuration: configuration))
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNet instance asynchronously with optional configuration.
|
||||
|
||||
Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread.
|
||||
|
||||
- parameters:
|
||||
- configuration: the desired model configuration
|
||||
- handler: the completion handler to be called when the model loading completes successfully or unsuccessfully
|
||||
*/
|
||||
class func load(configuration: MLModelConfiguration = MLModelConfiguration(), completionHandler handler: @escaping (Swift.Result<AgeNet, Error>) -> Void) {
|
||||
load(contentsOf: self.urlOfModelInThisBundle, configuration: configuration, completionHandler: handler)
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNet instance asynchronously with optional configuration.
|
||||
|
||||
Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread.
|
||||
|
||||
- parameters:
|
||||
- configuration: the desired model configuration
|
||||
*/
|
||||
class func load(configuration: MLModelConfiguration = MLModelConfiguration()) async throws -> AgeNet {
|
||||
try await load(contentsOf: self.urlOfModelInThisBundle, configuration: configuration)
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNet instance asynchronously with URL of the .mlmodelc directory with optional configuration.
|
||||
|
||||
Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread.
|
||||
|
||||
- parameters:
|
||||
- modelURL: the URL to the model
|
||||
- configuration: the desired model configuration
|
||||
- handler: the completion handler to be called when the model loading completes successfully or unsuccessfully
|
||||
*/
|
||||
class func load(contentsOf modelURL: URL, configuration: MLModelConfiguration = MLModelConfiguration(), completionHandler handler: @escaping (Swift.Result<AgeNet, Error>) -> Void) {
|
||||
MLModel.load(contentsOf: modelURL, configuration: configuration) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
handler(.failure(error))
|
||||
case .success(let model):
|
||||
handler(.success(AgeNet(model: model)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNet instance asynchronously with URL of the .mlmodelc directory with optional configuration.
|
||||
|
||||
Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread.
|
||||
|
||||
- parameters:
|
||||
- modelURL: the URL to the model
|
||||
- configuration: the desired model configuration
|
||||
*/
|
||||
class func load(contentsOf modelURL: URL, configuration: MLModelConfiguration = MLModelConfiguration()) async throws -> AgeNet {
|
||||
let model = try await MLModel.load(contentsOf: modelURL, configuration: configuration)
|
||||
return AgeNet(model: model)
|
||||
}
|
||||
|
||||
/**
|
||||
Make a prediction using the structured interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- input: the input to the prediction as AgeNetInput
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as AgeNetOutput
|
||||
*/
|
||||
func prediction(input: AgeNetInput) throws -> AgeNetOutput {
|
||||
try prediction(input: input, options: MLPredictionOptions())
|
||||
}
|
||||
|
||||
/**
|
||||
Make a prediction using the structured interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- input: the input to the prediction as AgeNetInput
|
||||
- options: prediction options
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as AgeNetOutput
|
||||
*/
|
||||
func prediction(input: AgeNetInput, options: MLPredictionOptions) throws -> AgeNetOutput {
|
||||
let outFeatures = try model.prediction(from: input, options: options)
|
||||
return AgeNetOutput(features: outFeatures)
|
||||
}
|
||||
|
||||
/**
|
||||
Make an asynchronous prediction using the structured interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- input: the input to the prediction as AgeNetInput
|
||||
- options: prediction options
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as AgeNetOutput
|
||||
*/
|
||||
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, visionOS 1.0, *)
|
||||
func prediction(input: AgeNetInput, options: MLPredictionOptions = MLPredictionOptions()) async throws -> AgeNetOutput {
|
||||
let outFeatures = try await model.prediction(from: input, options: options)
|
||||
return AgeNetOutput(features: outFeatures)
|
||||
}
|
||||
|
||||
/**
|
||||
Make a prediction using the convenience interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- input: color (kCVPixelFormatType_32BGRA) image buffer, 112 pixels wide by 112 pixels high
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as AgeNetOutput
|
||||
*/
|
||||
func prediction(input: CVPixelBuffer) throws -> AgeNetOutput {
|
||||
let input_ = AgeNetInput(input: input)
|
||||
return try prediction(input: input_)
|
||||
}
|
||||
|
||||
/**
|
||||
Make a batch prediction using the structured interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- inputs: the inputs to the prediction as [AgeNetInput]
|
||||
- options: prediction options
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as [AgeNetOutput]
|
||||
*/
|
||||
func predictions(inputs: [AgeNetInput], options: MLPredictionOptions = MLPredictionOptions()) throws -> [AgeNetOutput] {
|
||||
let batchIn = MLArrayBatchProvider(array: inputs)
|
||||
let batchOut = try model.predictions(from: batchIn, options: options)
|
||||
var results : [AgeNetOutput] = []
|
||||
results.reserveCapacity(inputs.count)
|
||||
for i in 0..<batchOut.count {
|
||||
let outProvider = batchOut.features(at: i)
|
||||
let result = AgeNetOutput(features: outProvider)
|
||||
results.append(result)
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
//
|
||||
// AgeNetLegacy.swift
|
||||
//
|
||||
// This file was automatically generated and should not be edited.
|
||||
//
|
||||
|
||||
import CoreML
|
||||
|
||||
|
||||
/// Model Prediction Input Type
|
||||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, visionOS 1.0, *)
|
||||
class AgeNetLegacyInput : MLFeatureProvider {
|
||||
|
||||
/// input as color (kCVPixelFormatType_32BGRA) image buffer, 112 pixels wide by 112 pixels high
|
||||
var input: CVPixelBuffer
|
||||
|
||||
var featureNames: Set<String> { ["input"] }
|
||||
|
||||
func featureValue(for featureName: String) -> MLFeatureValue? {
|
||||
if featureName == "input" {
|
||||
return MLFeatureValue(pixelBuffer: input)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
init(input: CVPixelBuffer) {
|
||||
self.input = input
|
||||
}
|
||||
|
||||
convenience init(inputWith input: CGImage) throws {
|
||||
self.init(input: try MLFeatureValue(cgImage: input, pixelsWide: 112, pixelsHigh: 112, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!)
|
||||
}
|
||||
|
||||
convenience init(inputAt input: URL) throws {
|
||||
self.init(input: try MLFeatureValue(imageAt: input, pixelsWide: 112, pixelsHigh: 112, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!)
|
||||
}
|
||||
|
||||
func setInput(with input: CGImage) throws {
|
||||
self.input = try MLFeatureValue(cgImage: input, pixelsWide: 112, pixelsHigh: 112, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!
|
||||
}
|
||||
|
||||
func setInput(with input: URL) throws {
|
||||
self.input = try MLFeatureValue(imageAt: input, pixelsWide: 112, pixelsHigh: 112, pixelFormatType: kCVPixelFormatType_32ARGB, options: nil).imageBufferValue!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Model Prediction Output Type
|
||||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, visionOS 1.0, *)
|
||||
class AgeNetLegacyOutput : MLFeatureProvider {
|
||||
|
||||
/// Source provided by CoreML
|
||||
private let provider : MLFeatureProvider
|
||||
|
||||
/// Identity as multidimensional array of floats
|
||||
var Identity: MLMultiArray {
|
||||
provider.featureValue(for: "Identity")!.multiArrayValue!
|
||||
}
|
||||
|
||||
/// Identity as multidimensional array of floats
|
||||
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
|
||||
var IdentityShapedArray: MLShapedArray<Float> {
|
||||
MLShapedArray<Float>(Identity)
|
||||
}
|
||||
|
||||
var featureNames: Set<String> {
|
||||
provider.featureNames
|
||||
}
|
||||
|
||||
func featureValue(for featureName: String) -> MLFeatureValue? {
|
||||
provider.featureValue(for: featureName)
|
||||
}
|
||||
|
||||
init(Identity: MLMultiArray) {
|
||||
self.provider = try! MLDictionaryFeatureProvider(dictionary: ["Identity" : MLFeatureValue(multiArray: Identity)])
|
||||
}
|
||||
|
||||
init(features: MLFeatureProvider) {
|
||||
self.provider = features
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Class for model loading and prediction
|
||||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, visionOS 1.0, *)
|
||||
class AgeNetLegacy {
|
||||
let model: MLModel
|
||||
|
||||
/// URL of model assuming it was installed in the same bundle as this class
|
||||
class var urlOfModelInThisBundle : URL {
|
||||
let bundle = Bundle(for: self)
|
||||
return bundle.url(forResource: "AgeNetLegacy", withExtension:"mlmodelc")!
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNetLegacy instance with an existing MLModel object.
|
||||
|
||||
Usually the application does not use this initializer unless it makes a subclass of AgeNetLegacy.
|
||||
Such application may want to use `MLModel(contentsOfURL:configuration:)` and `AgeNetLegacy.urlOfModelInThisBundle` to create a MLModel object to pass-in.
|
||||
|
||||
- parameters:
|
||||
- model: MLModel object
|
||||
*/
|
||||
init(model: MLModel) {
|
||||
self.model = model
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNetLegacy instance by automatically loading the model from the app's bundle.
|
||||
*/
|
||||
@available(*, deprecated, message: "Use init(configuration:) instead and handle errors appropriately.")
|
||||
convenience init() {
|
||||
try! self.init(contentsOf: type(of:self).urlOfModelInThisBundle)
|
||||
}
|
||||
|
||||
/**
|
||||
Construct a model with configuration
|
||||
|
||||
- parameters:
|
||||
- configuration: the desired model configuration
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
*/
|
||||
convenience init(configuration: MLModelConfiguration) throws {
|
||||
try self.init(contentsOf: type(of:self).urlOfModelInThisBundle, configuration: configuration)
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNetLegacy instance with explicit path to mlmodelc file
|
||||
- parameters:
|
||||
- modelURL: the file url of the model
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
*/
|
||||
convenience init(contentsOf modelURL: URL) throws {
|
||||
try self.init(model: MLModel(contentsOf: modelURL))
|
||||
}
|
||||
|
||||
/**
|
||||
Construct a model with URL of the .mlmodelc directory and configuration
|
||||
|
||||
- parameters:
|
||||
- modelURL: the file url of the model
|
||||
- configuration: the desired model configuration
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
*/
|
||||
convenience init(contentsOf modelURL: URL, configuration: MLModelConfiguration) throws {
|
||||
try self.init(model: MLModel(contentsOf: modelURL, configuration: configuration))
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNetLegacy instance asynchronously with optional configuration.
|
||||
|
||||
Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread.
|
||||
|
||||
- parameters:
|
||||
- configuration: the desired model configuration
|
||||
- handler: the completion handler to be called when the model loading completes successfully or unsuccessfully
|
||||
*/
|
||||
@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *)
|
||||
class func load(configuration: MLModelConfiguration = MLModelConfiguration(), completionHandler handler: @escaping (Swift.Result<AgeNetLegacy, Error>) -> Void) {
|
||||
load(contentsOf: self.urlOfModelInThisBundle, configuration: configuration, completionHandler: handler)
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNetLegacy instance asynchronously with optional configuration.
|
||||
|
||||
Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread.
|
||||
|
||||
- parameters:
|
||||
- configuration: the desired model configuration
|
||||
*/
|
||||
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
|
||||
class func load(configuration: MLModelConfiguration = MLModelConfiguration()) async throws -> AgeNetLegacy {
|
||||
try await load(contentsOf: self.urlOfModelInThisBundle, configuration: configuration)
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNetLegacy instance asynchronously with URL of the .mlmodelc directory with optional configuration.
|
||||
|
||||
Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread.
|
||||
|
||||
- parameters:
|
||||
- modelURL: the URL to the model
|
||||
- configuration: the desired model configuration
|
||||
- handler: the completion handler to be called when the model loading completes successfully or unsuccessfully
|
||||
*/
|
||||
@available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *)
|
||||
class func load(contentsOf modelURL: URL, configuration: MLModelConfiguration = MLModelConfiguration(), completionHandler handler: @escaping (Swift.Result<AgeNetLegacy, Error>) -> Void) {
|
||||
MLModel.load(contentsOf: modelURL, configuration: configuration) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
handler(.failure(error))
|
||||
case .success(let model):
|
||||
handler(.success(AgeNetLegacy(model: model)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Construct AgeNetLegacy instance asynchronously with URL of the .mlmodelc directory with optional configuration.
|
||||
|
||||
Model loading may take time when the model content is not immediately available (e.g. encrypted model). Use this factory method especially when the caller is on the main thread.
|
||||
|
||||
- parameters:
|
||||
- modelURL: the URL to the model
|
||||
- configuration: the desired model configuration
|
||||
*/
|
||||
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
|
||||
class func load(contentsOf modelURL: URL, configuration: MLModelConfiguration = MLModelConfiguration()) async throws -> AgeNetLegacy {
|
||||
let model = try await MLModel.load(contentsOf: modelURL, configuration: configuration)
|
||||
return AgeNetLegacy(model: model)
|
||||
}
|
||||
|
||||
/**
|
||||
Make a prediction using the structured interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- input: the input to the prediction as AgeNetLegacyInput
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as AgeNetLegacyOutput
|
||||
*/
|
||||
func prediction(input: AgeNetLegacyInput) throws -> AgeNetLegacyOutput {
|
||||
try prediction(input: input, options: MLPredictionOptions())
|
||||
}
|
||||
|
||||
/**
|
||||
Make a prediction using the structured interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- input: the input to the prediction as AgeNetLegacyInput
|
||||
- options: prediction options
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as AgeNetLegacyOutput
|
||||
*/
|
||||
func prediction(input: AgeNetLegacyInput, options: MLPredictionOptions) throws -> AgeNetLegacyOutput {
|
||||
let outFeatures = try model.prediction(from: input, options: options)
|
||||
return AgeNetLegacyOutput(features: outFeatures)
|
||||
}
|
||||
|
||||
/**
|
||||
Make an asynchronous prediction using the structured interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- input: the input to the prediction as AgeNetLegacyInput
|
||||
- options: prediction options
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as AgeNetLegacyOutput
|
||||
*/
|
||||
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, visionOS 1.0, *)
|
||||
func prediction(input: AgeNetLegacyInput, options: MLPredictionOptions = MLPredictionOptions()) async throws -> AgeNetLegacyOutput {
|
||||
let outFeatures = try await model.prediction(from: input, options: options)
|
||||
return AgeNetLegacyOutput(features: outFeatures)
|
||||
}
|
||||
|
||||
/**
|
||||
Make a prediction using the convenience interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- input: color (kCVPixelFormatType_32BGRA) image buffer, 112 pixels wide by 112 pixels high
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as AgeNetLegacyOutput
|
||||
*/
|
||||
func prediction(input: CVPixelBuffer) throws -> AgeNetLegacyOutput {
|
||||
let input_ = AgeNetLegacyInput(input: input)
|
||||
return try prediction(input: input_)
|
||||
}
|
||||
|
||||
/**
|
||||
Make a batch prediction using the structured interface
|
||||
|
||||
It uses the default function if the model has multiple functions.
|
||||
|
||||
- parameters:
|
||||
- inputs: the inputs to the prediction as [AgeNetLegacyInput]
|
||||
- options: prediction options
|
||||
|
||||
- throws: an NSError object that describes the problem
|
||||
|
||||
- returns: the result of the prediction as [AgeNetLegacyOutput]
|
||||
*/
|
||||
func predictions(inputs: [AgeNetLegacyInput], options: MLPredictionOptions = MLPredictionOptions()) throws -> [AgeNetLegacyOutput] {
|
||||
let batchIn = MLArrayBatchProvider(array: inputs)
|
||||
let batchOut = try model.predictions(from: batchIn, options: options)
|
||||
var results : [AgeNetLegacyOutput] = []
|
||||
results.reserveCapacity(inputs.count)
|
||||
for i in 0..<batchOut.count {
|
||||
let outProvider = batchOut.features(at: i)
|
||||
let result = AgeNetLegacyOutput(features: outProvider)
|
||||
results.append(result)
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import FileMediaResourceStatus
|
||||
import ZipArchive
|
||||
|
||||
private let queue = Queue()
|
||||
|
||||
public enum AgeVerificationAvailability {
|
||||
case available(String, Bool)
|
||||
case progress(Float)
|
||||
case unavailable
|
||||
}
|
||||
|
||||
private var forceCoreMLVariant: Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
private func modelPath() -> String {
|
||||
return NSTemporaryDirectory() + "AgeNet.mlmodelc"
|
||||
}
|
||||
private let modelPeer = "agecomputation"
|
||||
|
||||
private func legacyModelPath() -> String {
|
||||
return NSTemporaryDirectory() + "AgeNetLegacy.mlmodelc"
|
||||
}
|
||||
private let legacyModelPeer = "agelegacycomputation"
|
||||
|
||||
public func ageVerificationAvailability(context: AccountContext) -> Signal<AgeVerificationAvailability, NoError> {
|
||||
let compiledModelPath: String
|
||||
let modelPeerName: String
|
||||
let isLegacy: Bool
|
||||
if #available(iOS 15.0, *) {
|
||||
compiledModelPath = modelPath()
|
||||
modelPeerName = modelPeer
|
||||
isLegacy = false
|
||||
} else {
|
||||
compiledModelPath = legacyModelPath()
|
||||
modelPeerName = legacyModelPeer
|
||||
isLegacy = true
|
||||
}
|
||||
if FileManager.default.fileExists(atPath: compiledModelPath) {
|
||||
return .single(.available(compiledModelPath, isLegacy))
|
||||
}
|
||||
return context.engine.peers.resolvePeerByName(name: modelPeerName, referrer: nil)
|
||||
|> mapToSignal { result -> Signal<AgeVerificationAvailability, NoError> in
|
||||
guard case let .result(maybePeer) = result else {
|
||||
return .complete()
|
||||
}
|
||||
guard let peer = maybePeer else {
|
||||
return .single(.unavailable)
|
||||
}
|
||||
|
||||
return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: peer.id, threadId: nil), index: .lowerBound, anchorIndex: .lowerBound, count: 5, fixedCombinedReadStates: nil)
|
||||
|> mapToSignal { view -> Signal<(TelegramMediaFile, EngineMessage)?, NoError> in
|
||||
if !view.0.isLoading {
|
||||
if let message = view.0.entries.last?.message, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
|
||||
return .single((file, EngineMessage(message)))
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> take(1)
|
||||
|> mapToSignal { maybeFileAndMessage -> Signal<AgeVerificationAvailability, NoError> in
|
||||
if let (file, message) = maybeFileAndMessage {
|
||||
let fetchedData = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .file, reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource))
|
||||
|
||||
enum FetchStatus {
|
||||
case completed(String)
|
||||
case progress(Float)
|
||||
case failed
|
||||
}
|
||||
|
||||
let fetchStatus = Signal<FetchStatus, NoError> { subscriber in
|
||||
let fetchedDisposable = fetchedData.start()
|
||||
let resourceDataDisposable = context.account.postbox.mediaBox.resourceData(file.resource, attemptSynchronously: false).start(next: { next in
|
||||
if next.complete {
|
||||
SSZipArchive.unzipFile(atPath: next.path, toDestination: NSTemporaryDirectory())
|
||||
subscriber.putNext(.completed(compiledModelPath))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}, error: subscriber.putError, completed: subscriber.putCompletion)
|
||||
let progressDisposable = messageFileMediaResourceStatus(context: context, file: file, message: message, isRecentActions: false).start(next: { status in
|
||||
switch status.fetchStatus {
|
||||
case let .Remote(progress), let .Fetching(_, progress), let .Paused(progress):
|
||||
subscriber.putNext(.progress(progress))
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
return ActionDisposable {
|
||||
fetchedDisposable.dispose()
|
||||
resourceDataDisposable.dispose()
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
return fetchStatus
|
||||
|> mapToSignal { status -> Signal<AgeVerificationAvailability, NoError> in
|
||||
switch status {
|
||||
case .completed:
|
||||
return .single(.available(compiledModelPath, isLegacy))
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
case .failed:
|
||||
return .single(.unavailable)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(.unavailable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,469 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Markdown
|
||||
import TextFormat
|
||||
import TelegramPresentationData
|
||||
import ViewControllerComponent
|
||||
import SheetComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import BundleIconComponent
|
||||
import ButtonComponent
|
||||
import AccountContext
|
||||
import PresentationDataUtils
|
||||
import TelegramUIPreferences
|
||||
import UndoUI
|
||||
import DeviceAccess
|
||||
|
||||
public func requireAgeVerification(context: AccountContext) -> Bool {
|
||||
if let value = context.currentAppConfiguration.with({ $0 }).data?["need_age_video_verification"] as? Bool, value {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func requireAgeVerification(context: AccountContext, peer: EnginePeer) -> Signal<Bool, NoError> {
|
||||
if requireAgeVerification(context: context), peer._asPeer().hasSensitiveContent(platform: "ios") {
|
||||
return context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.ContentSettings())
|
||||
|> map { contentSettings in
|
||||
if !contentSettings.ignoreContentRestrictionReasons.contains("sensitive") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
private final class SheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let dismiss: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
dismiss: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
var cachedCloseImage: (UIImage, PresentationTheme)?
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State()
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(RoundedRectangle.self)
|
||||
let icon = Child(ZStack<Empty>.self)
|
||||
let closeButton = Child(Button.self)
|
||||
let title = Child(Text.self)
|
||||
let text = Child(BalancedTextComponent.self)
|
||||
|
||||
let button = Child(ButtonComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let component = context.component
|
||||
let state = context.state
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = presentationData.theme
|
||||
let strings = presentationData.strings
|
||||
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
|
||||
|
||||
let background = background.update(
|
||||
component: RoundedRectangle(color: theme.actionSheet.opaqueItemBackgroundColor, cornerRadius: 8.0),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
|
||||
)
|
||||
|
||||
let icon = icon.update(
|
||||
component: ZStack([
|
||||
AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(RoundedRectangle(color: theme.list.itemCheckColors.fillColor, cornerRadius: 45.0, size: CGSize(width: 90.0, height: 90.0)))
|
||||
),
|
||||
AnyComponentWithIdentity(
|
||||
id: AnyHashable(1),
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Settings/FaceVerification",
|
||||
tintColor: theme.list.itemCheckColors.foregroundColor
|
||||
))
|
||||
)
|
||||
]),
|
||||
availableSize: CGSize(width: 90.0, height: 90.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(icon
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: icon.size.height / 2.0 + 31.0))
|
||||
)
|
||||
|
||||
let closeImage: UIImage
|
||||
if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme {
|
||||
closeImage = image
|
||||
} else {
|
||||
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
|
||||
state.cachedCloseImage = (closeImage, theme)
|
||||
}
|
||||
let closeButton = closeButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(Image(image: closeImage)),
|
||||
action: {
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(closeButton
|
||||
.position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0))
|
||||
)
|
||||
|
||||
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
|
||||
|
||||
contentSize.height += 124.0
|
||||
|
||||
let title = title.update(
|
||||
component: Text(text: strings.AgeVerification_Title, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor),
|
||||
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += title.size.height
|
||||
contentSize.height += 13.0
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
let textColor = theme.actionSheet.primaryTextColor
|
||||
let linkColor = theme.actionSheet.controlAccentColor
|
||||
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
})
|
||||
|
||||
var textString = environment.strings.AgeVerification_Text
|
||||
if let code = component.context.currentAppConfiguration.with({ $0 }).data?["verify_age_country"] as? String {
|
||||
let key = "AgeVerification.Text.\(code)"
|
||||
if let string = environment.strings.primaryComponent.dict[key] {
|
||||
textString = string
|
||||
}
|
||||
}
|
||||
|
||||
let text = text.update(
|
||||
component: BalancedTextComponent(
|
||||
text: .markdown(
|
||||
text: textString,
|
||||
attributes: markdownAttributes
|
||||
),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2
|
||||
),
|
||||
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(text
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += text.size.height
|
||||
contentSize.height += 23.0
|
||||
|
||||
let controller = environment.controller() as? AgeVerificationScreen
|
||||
|
||||
let button = button.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
cornerRadius: 10.0
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.AgeVerification_Verify, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: { [weak controller] in
|
||||
controller?.complete(result: true)
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 50),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(button
|
||||
.clipsToBounds(true)
|
||||
.cornerRadius(10.0)
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += button.size.height
|
||||
|
||||
contentSize.height += 48.0
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class AgeVerificationSheetComponent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
init(
|
||||
context: AccountContext
|
||||
) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
static func ==(lhs: AgeVerificationSheetComponent, rhs: AgeVerificationSheetComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let sheet = Child(SheetComponent<(EnvironmentType)>.self)
|
||||
let animateOut = StoredActionSlot(Action<Void>.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
let sheet = sheet.update(
|
||||
component: SheetComponent<EnvironmentType>(
|
||||
content: AnyComponent<EnvironmentType>(SheetContent(
|
||||
context: context.component.context,
|
||||
dismiss: {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() as? AgeVerificationScreen {
|
||||
controller.complete(result: false)
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
|
||||
followContentSizeChanges: true,
|
||||
clipsContent: true,
|
||||
animateOut: animateOut
|
||||
),
|
||||
environment: {
|
||||
environment
|
||||
SheetComponentEnvironment(
|
||||
isDisplaying: environment.value.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||
dismiss: { animated in
|
||||
if animated {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() as? AgeVerificationScreen {
|
||||
controller.complete(result: false)
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if let controller = controller() as? AgeVerificationScreen {
|
||||
controller.complete(result: false)
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(sheet
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class AgeVerificationScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
private let completion: (Bool, Signal<AgeVerificationAvailability, NoError>) -> Void
|
||||
|
||||
private let promise = Promise<AgeVerificationAvailability>()
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
completion: @escaping (Bool, Signal<AgeVerificationAvailability, NoError>) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.completion = completion
|
||||
|
||||
self.promise.set(ageVerificationAvailability(context: context))
|
||||
|
||||
super.init(
|
||||
context: context,
|
||||
component: AgeVerificationSheetComponent(
|
||||
context: context
|
||||
),
|
||||
navigationBarAppearance: .none,
|
||||
statusBarStyle: .ignore,
|
||||
theme: .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var didComplete = false
|
||||
fileprivate func complete(result: Bool) {
|
||||
guard !self.didComplete else {
|
||||
return
|
||||
}
|
||||
|
||||
if result {
|
||||
let context = self.context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
DeviceAccess.authorizeAccess(to: .camera(.ageVerification), presentationData: presentationData, present: { c, a in
|
||||
c.presentationArguments = a
|
||||
context.sharedContext.mainWindow?.present(c, on: .root)
|
||||
}, openSettings: {
|
||||
context.sharedContext.applicationBindings.openSettings()
|
||||
}, { [weak self] granted in
|
||||
guard let self, granted else {
|
||||
return
|
||||
}
|
||||
self.didComplete = true
|
||||
self.completion(true, self.promise.get())
|
||||
self.dismissAnimated()
|
||||
})
|
||||
} else {
|
||||
self.didComplete = true
|
||||
self.completion(false, self.promise.get())
|
||||
}
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(foregroundColor.cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 10.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 20.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
|
||||
public func presentAgeVerification(context: AccountContext, parentController: ViewController, completion: @escaping () -> Void) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.ageVerificationState)
|
||||
) |> deliverOnMainQueue).start(next: { [weak parentController] ageVerificationStatePreference in
|
||||
let state = ageVerificationStatePreference?.get(AgeVerificationState.self) ?? AgeVerificationState.default
|
||||
if state.verificationPassed {
|
||||
completion()
|
||||
} else {
|
||||
let infoScreen = AgeVerificationScreen(context: context, completion: { [weak parentController] check, availability in
|
||||
if check {
|
||||
let scanScreen = FaceScanScreen(context: context, availability: availability, completion: { [weak parentController] passed in
|
||||
if passed {
|
||||
let _ = updateAgeVerificationState(engine: context.engine, { _ in
|
||||
return AgeVerificationState(verificationPassed: passed)
|
||||
}).start()
|
||||
completion()
|
||||
|
||||
let navigationController = parentController?.navigationController
|
||||
Queue.mainQueue().after(2.0) {
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AgeVerification_Success_Title, text: presentationData.strings.AgeVerification_Success_Text, cancel: nil, destructive: false), action: { _ in return true })
|
||||
(navigationController?.viewControllers.last as? ViewController)?.present(controller, in: .window(.root))
|
||||
}
|
||||
} else {
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_banned", scale: 0.066, colors: [:], title: presentationData.strings.AgeVerification_Fail_Title, text: presentationData.strings.AgeVerification_Fail_Text, customUndoText: nil, timeout: nil), action: { _ in return true })
|
||||
parentController?.present(controller, in: .window(.root))
|
||||
}
|
||||
})
|
||||
parentController?.push(scanScreen)
|
||||
}
|
||||
})
|
||||
parentController?.push(infoScreen)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func updateAgeVerificationState(engine: TelegramEngine, _ f: @escaping (AgeVerificationState) -> AgeVerificationState) -> Signal<Never, NoError> {
|
||||
return engine.preferences.update(id: ApplicationSpecificPreferencesKeys.ageVerificationState, { entry in
|
||||
let currentSettings: AgeVerificationState
|
||||
if let entry = entry?.get(AgeVerificationState.self) {
|
||||
currentSettings = entry
|
||||
} else {
|
||||
currentSettings = .default
|
||||
}
|
||||
return SharedPreferencesEntry(f(currentSettings))
|
||||
})
|
||||
}
|
||||
|
||||
public struct AgeVerificationState: Equatable, Codable {
|
||||
public var verificationPassed: Bool
|
||||
|
||||
public static var `default`: AgeVerificationState {
|
||||
return AgeVerificationState(verificationPassed: false)
|
||||
}
|
||||
|
||||
public init(verificationPassed: Bool) {
|
||||
self.verificationPassed = verificationPassed
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
self.verificationPassed = (try container.decode(Int32.self, forKey: "verificationPassed")) != 0
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
try container.encode((self.verificationPassed ? 1 : 0) as Int32, forKey: "verificationPassed")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,794 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AccountContext
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Camera
|
||||
import CoreImage
|
||||
import TelegramPresentationData
|
||||
import TelegramCore
|
||||
import Markdown
|
||||
import TextFormat
|
||||
import PresentationDataUtils
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import Vision
|
||||
import AVFoundation
|
||||
import AppBundle
|
||||
import ZipArchive
|
||||
import PlainButtonComponent
|
||||
import MultilineTextComponent
|
||||
|
||||
private let requiredAge = 18
|
||||
|
||||
final class FaceScanScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let availability: Signal<AgeVerificationAvailability, NoError>
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
availability: Signal<AgeVerificationAvailability, NoError>
|
||||
) {
|
||||
self.context = context
|
||||
self.availability = availability
|
||||
}
|
||||
|
||||
static func ==(lhs: FaceScanScreenComponent, rhs: FaceScanScreenComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, AVCaptureVideoDataOutputSampleBufferDelegate {
|
||||
private let captureSession = AVCaptureSession()
|
||||
private lazy var previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
|
||||
private let videoDataOutput = AVCaptureVideoDataOutput()
|
||||
private let videoDataOutputQueue = DispatchQueue(label: "VideoDataOutput", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
|
||||
private var faceDetectionRequest: VNDetectFaceRectanglesRequest!
|
||||
|
||||
private let overlayView = UIView()
|
||||
private let cutoutLayer = CAShapeLayer()
|
||||
private let frameView = FrameView()
|
||||
|
||||
private let numberOfStripes = 16
|
||||
private var completedAngles: Set<Int> = []
|
||||
|
||||
private let instruction = ComponentView<Empty>()
|
||||
private let cancel = ComponentView<Empty>()
|
||||
|
||||
private var currentFaceImage: CIImage?
|
||||
|
||||
private enum State {
|
||||
case waitingForFace
|
||||
case positioning
|
||||
case readyToStart
|
||||
case tracking
|
||||
case completed
|
||||
}
|
||||
private var processState: State = .waitingForFace
|
||||
private var transitioningToViewFinder = false
|
||||
|
||||
private var lastFaceYaw: NSNumber?
|
||||
private var lastFacePitch: NSNumber?
|
||||
private var lastFaceRoll: NSNumber?
|
||||
private var centerYaw: Double = 0
|
||||
private var centerPitch: Double = 0
|
||||
private var centerRoll: Double = 0
|
||||
private let motionThreshold: Double = 0.07
|
||||
|
||||
private var faceDetectionTimer: Foundation.Timer?
|
||||
private var segmentTimer: Foundation.Timer?
|
||||
private var currentSegment: Int?
|
||||
private let segmentDwellTime: TimeInterval = 0.05
|
||||
private let positioningTime: TimeInterval = 1.0
|
||||
|
||||
private var horizontalGuideLines: [CAShapeLayer] = []
|
||||
private var verticalGuideLines: [CAShapeLayer] = []
|
||||
private let maxGuideLines = 30
|
||||
private let guideLineFadeDuration: TimeInterval = 0.3
|
||||
private var lastGuideLineUpdate: Date = Date()
|
||||
private let guideLineUpdateInterval: TimeInterval = 0.01
|
||||
private var lastGuideLineYaw: Double = 0
|
||||
private var lastGuideLinePitch: Double = 0
|
||||
private let angleThreshold: Double = 0.01
|
||||
|
||||
private var ageModel: VNCoreMLModel?
|
||||
private var ages: [Double] = []
|
||||
|
||||
private var availabilityDisposable: Disposable?
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: FaceScanScreenComponent?
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .black
|
||||
|
||||
self.previewLayer.backgroundColor = UIColor.red.cgColor
|
||||
self.previewLayer.videoGravity = .resizeAspectFill
|
||||
self.layer.addSublayer(previewLayer)
|
||||
|
||||
self.overlayView.backgroundColor = UIColor.black
|
||||
self.addSubview(overlayView)
|
||||
|
||||
self.cutoutLayer.fillRule = .evenOdd
|
||||
self.overlayView.layer.mask = self.cutoutLayer
|
||||
|
||||
self.addSubview(self.frameView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.availabilityDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func setupModel(availability: Signal<AgeVerificationAvailability, NoError>) {
|
||||
self.availabilityDisposable = (availability
|
||||
|> deliverOnMainQueue).start(next: { [weak self] availability in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if case let .available(path, isLegacy) = availability {
|
||||
if isLegacy {
|
||||
if let model = try? AgeNetLegacy(contentsOf: URL(fileURLWithPath: path)).model {
|
||||
self.ageModel = try? VNCoreMLModel(for: model)
|
||||
}
|
||||
} else if #available(iOS 15.0, *) {
|
||||
if let model = try? AgeNet(contentsOf: URL(fileURLWithPath: path)).model {
|
||||
self.ageModel = try? VNCoreMLModel(for: model)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func extractFaceImage(from pixelBuffer: CVPixelBuffer, faceObservation: VNFaceObservation) -> CIImage? {
|
||||
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
||||
let imageSize = ciImage.extent.size
|
||||
let visionRect = faceObservation.boundingBox
|
||||
|
||||
let boundingBox = CGRect(x: 1.0 - visionRect.maxY, y: 1.0 - visionRect.maxX, width: visionRect.height, height: visionRect.width)
|
||||
let faceRect = CGRect(
|
||||
x: boundingBox.minX * imageSize.width,
|
||||
y: boundingBox.minY * imageSize.height,
|
||||
width: boundingBox.width * imageSize.width,
|
||||
height: boundingBox.height * imageSize.height
|
||||
)
|
||||
|
||||
let padding: CGFloat = 0.1
|
||||
let paddingX = faceRect.width * padding
|
||||
let paddingY = faceRect.height * padding
|
||||
|
||||
let paddedRect = CGRect(
|
||||
x: max(0, faceRect.minX - paddingX),
|
||||
y: max(0, faceRect.minY - paddingY),
|
||||
width: min(imageSize.width - max(0, faceRect.minX - paddingX), faceRect.width + 2 * paddingX),
|
||||
height: min(imageSize.height - max(0, faceRect.minY - paddingY), faceRect.height + 2 * paddingY)
|
||||
)
|
||||
|
||||
let croppedImage = ciImage.cropped(to: paddedRect)
|
||||
let rotatedImage = croppedImage.oriented(.leftMirrored)
|
||||
|
||||
return rotatedImage
|
||||
}
|
||||
|
||||
private func setupCamera() {
|
||||
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let input = try AVCaptureDeviceInput(device: device)
|
||||
|
||||
self.captureSession.beginConfiguration()
|
||||
self.captureSession.sessionPreset = .high
|
||||
|
||||
if self.captureSession.canAddInput(input) {
|
||||
self.captureSession.addInput(input)
|
||||
}
|
||||
|
||||
if self.captureSession.canAddOutput(self.videoDataOutput) {
|
||||
self.captureSession.addOutput(self.videoDataOutput)
|
||||
self.videoDataOutput.alwaysDiscardsLateVideoFrames = true
|
||||
self.videoDataOutput.setSampleBufferDelegate(self, queue: self.videoDataOutputQueue)
|
||||
}
|
||||
|
||||
self.captureSession.commitConfiguration()
|
||||
} catch {
|
||||
print("Failed to setup camera: \(error)")
|
||||
}
|
||||
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
self.captureSession.startRunning()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupVision() {
|
||||
self.faceDetectionRequest = VNDetectFaceRectanglesRequest { [weak self] request, error in
|
||||
guard error == nil else { return }
|
||||
Queue.mainQueue().async {
|
||||
self?.handleFaceDetection(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleFaceDetection(_ request: VNRequest) {
|
||||
guard #available(iOS 15.0, *) else {
|
||||
return
|
||||
}
|
||||
guard let observations = request.results as? [VNFaceObservation], let face = observations.first else {
|
||||
if self.processState == .tracking || self.processState == .readyToStart {
|
||||
self.resetTracking()
|
||||
}
|
||||
self.currentFaceImage = nil
|
||||
return
|
||||
}
|
||||
|
||||
guard let yaw = face.yaw,
|
||||
let pitch = face.pitch,
|
||||
let roll = face.roll else { return }
|
||||
|
||||
let faceCenter = CGPoint(x: face.boundingBox.midX, y: face.boundingBox.midY)
|
||||
let targetCenter = CGPoint(x: 0.5, y: 0.5)
|
||||
let distance = sqrt(pow(faceCenter.x - targetCenter.x, 2) + pow(faceCenter.y - targetCenter.y, 2))
|
||||
|
||||
if distance < 0.35 {
|
||||
switch processState {
|
||||
case .waitingForFace:
|
||||
self.processState = .positioning
|
||||
self.faceDetectionTimer = Timer.scheduledTimer(withTimeInterval: self.positioningTime, repeats: false) { [weak self] _ in
|
||||
self?.processState = .readyToStart
|
||||
|
||||
self?.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
case .positioning:
|
||||
break
|
||||
case .readyToStart:
|
||||
self.centerYaw = yaw.doubleValue
|
||||
self.centerPitch = pitch.doubleValue
|
||||
self.centerRoll = roll.doubleValue
|
||||
self.processState = .tracking
|
||||
case .tracking:
|
||||
self.trackHeadOrientation(yaw: yaw, pitch: pitch, roll: roll)
|
||||
case .completed:
|
||||
break
|
||||
}
|
||||
} else if self.processState == .tracking {
|
||||
self.resetTracking()
|
||||
}
|
||||
}
|
||||
|
||||
private func trackHeadOrientation(yaw: NSNumber, pitch: NSNumber, roll: NSNumber) {
|
||||
let relativeYaw = yaw.doubleValue - self.centerYaw
|
||||
let relativePitch = pitch.doubleValue - self.centerPitch
|
||||
|
||||
//self.updateTrailingGuideLines(yaw: relativeYaw, pitch: relativePitch)
|
||||
|
||||
if let lastYaw = self.lastFaceYaw, let lastPitch = self.lastFacePitch {
|
||||
let yawChange = abs(yaw.doubleValue - lastYaw.doubleValue)
|
||||
let pitchChange = abs(pitch.doubleValue - lastPitch.doubleValue)
|
||||
|
||||
if yawChange < 0.02 && pitchChange < 0.02 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let mirroredYaw = -relativeYaw
|
||||
let flippedPitch = relativePitch
|
||||
|
||||
let angle = atan2(mirroredYaw, flippedPitch) + .pi
|
||||
let normalizedAngle = angle / (2 * .pi)
|
||||
let segmentIndex = Int(normalizedAngle * Double(self.numberOfStripes)) % self.numberOfStripes
|
||||
|
||||
let rotationMagnitude = sqrt(relativeYaw * relativeYaw + relativePitch * relativePitch)
|
||||
if rotationMagnitude > self.motionThreshold {
|
||||
if self.currentSegment != segmentIndex {
|
||||
self.currentSegment = segmentIndex
|
||||
|
||||
self.segmentTimer?.invalidate()
|
||||
self.segmentTimer = Timer.scheduledTimer(withTimeInterval: self.segmentDwellTime, repeats: false) { _ in
|
||||
self.fillSegment(segmentIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.lastFaceYaw = yaw
|
||||
self.lastFacePitch = pitch
|
||||
self.lastFaceRoll = roll
|
||||
}
|
||||
|
||||
private func fillSegment(_ segmentIndex: Int) {
|
||||
guard !self.completedAngles.contains(segmentIndex) else {
|
||||
return
|
||||
}
|
||||
self.completedAngles.insert(segmentIndex)
|
||||
|
||||
if self.completedAngles.count >= self.numberOfStripes {
|
||||
Queue.mainQueue().after(0.3, {
|
||||
self.processState = .completed
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
|
||||
Queue.mainQueue().after(1.0) {
|
||||
if !self.ages.isEmpty {
|
||||
let averageAge = self.ages.reduce(0, +) / Double(self.ages.count)
|
||||
if let environment = self.environment, let controller = environment.controller() as? FaceScanScreen {
|
||||
controller.completion(averageAge >= Double(requiredAge))
|
||||
controller.dismiss(animated: true)
|
||||
}
|
||||
} else {
|
||||
self.completedAngles.removeAll()
|
||||
self.processState = .tracking
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
|
||||
private func resetTracking() {
|
||||
self.faceDetectionTimer?.invalidate()
|
||||
self.segmentTimer?.invalidate()
|
||||
|
||||
if self.processState != .waitingForFace && self.processState != .positioning {
|
||||
self.transitioningToViewFinder = true
|
||||
}
|
||||
self.processState = .waitingForFace
|
||||
self.completedAngles.removeAll()
|
||||
self.currentSegment = nil
|
||||
self.lastFaceYaw = nil
|
||||
self.lastFacePitch = nil
|
||||
self.lastFaceRoll = nil
|
||||
|
||||
self.currentFaceImage = nil
|
||||
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
|
||||
private var tick = 0
|
||||
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
|
||||
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
|
||||
|
||||
let requestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: .leftMirrored)
|
||||
do {
|
||||
try requestHandler.perform([self.faceDetectionRequest])
|
||||
|
||||
if let observations = self.faceDetectionRequest.results, let faceObservation = observations.first {
|
||||
tick += 1
|
||||
if tick < 33 {
|
||||
return
|
||||
}
|
||||
tick = 0
|
||||
|
||||
self.currentFaceImage = self.extractFaceImage(from: pixelBuffer, faceObservation: faceObservation)
|
||||
self.processFace()
|
||||
}
|
||||
} catch {
|
||||
print("Failed to perform request: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func processFace() {
|
||||
guard let faceImage = self.currentFaceImage else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let model = self.ageModel else {
|
||||
return
|
||||
}
|
||||
|
||||
let request = VNCoreMLRequest(model: model) { [weak self] request, error in
|
||||
if let results = request.results as? [VNCoreMLFeatureValueObservation], let ageObservation = results.last {
|
||||
let age = ageObservation.featureValue.multiArrayValue?[0].doubleValue ?? 0
|
||||
|
||||
Queue.mainQueue().async {
|
||||
self?.ages.append(age)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let handler = VNImageRequestHandler(ciImage: faceImage)
|
||||
DispatchQueue.global(qos: .userInteractive).async {
|
||||
do {
|
||||
try handler.perform([request])
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: FaceScanScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
if self.component == nil {
|
||||
self.setupModel(availability: component.availability)
|
||||
self.setupCamera()
|
||||
self.setupVision()
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
self.environment = environment
|
||||
|
||||
let theme = environment.theme
|
||||
let strings = environment.strings
|
||||
|
||||
self.overlayView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
self.cutoutLayer.frame = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
let path = CGMutablePath(rect: overlayView.bounds, transform: nil)
|
||||
let radius: CGFloat = 130.0
|
||||
|
||||
let widthRadius = ceil(radius * 1.05)
|
||||
let heightRadius = widthRadius //radius //floor(widthRadius * 1.17778)
|
||||
|
||||
let center = CGPoint(x: availableSize.width / 2, y: environment.statusBarHeight + 10.0 + widthRadius * 1.3)
|
||||
|
||||
var previewScale = 1.0
|
||||
if self.processState == .tracking || self.processState == .readyToStart || self.processState == .completed || self.transitioningToViewFinder {
|
||||
let circlePath = CGPath(roundedRect: CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2), cornerWidth: radius, cornerHeight: radius, transform: nil)
|
||||
path.addPath(circlePath)
|
||||
|
||||
previewScale = 0.75
|
||||
} else {
|
||||
let rectanglePath = CGPath(roundedRect: CGRect(x: center.x - widthRadius, y: center.y - heightRadius, width: widthRadius * 2, height: heightRadius * 2), cornerWidth: 20, cornerHeight: 20, transform: nil)
|
||||
path.addPath(rectanglePath)
|
||||
}
|
||||
transition.setShapeLayerPath(layer: self.cutoutLayer, path: path)
|
||||
|
||||
self.previewLayer.bounds = CGRect(origin: .zero, size: availableSize)
|
||||
self.previewLayer.position = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0 - 200)
|
||||
transition.setTransform(layer: self.previewLayer, transform: CATransform3DMakeScale(previewScale, previewScale, 1.0))
|
||||
|
||||
let frameViewSize = CGSize(width: 330.0, height: 330.0)
|
||||
let frameViewFrame = CGRect(x: (availableSize.width - frameViewSize.width) / 2.0, y: center.y - frameViewSize.height * 0.5, width: frameViewSize.width, height: frameViewSize.height)
|
||||
self.frameView.frame = frameViewFrame
|
||||
self.frameView.update(size: frameViewFrame.size)
|
||||
|
||||
//TODO:localize
|
||||
var instructionString = environment.strings.FaceScan_Instruction_Position
|
||||
switch self.processState {
|
||||
case .waitingForFace, .positioning:
|
||||
self.frameView.update(state: .viewFinder, intermediateCompletion: { [weak self] in
|
||||
if let self {
|
||||
self.transitioningToViewFinder = false
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
}, transition: .spring(duration: 0.3))
|
||||
instructionString = environment.strings.FaceScan_Instruction_Position
|
||||
case .readyToStart:
|
||||
self.frameView.update(state: .segments(Set()), transition: .spring(duration: 0.3))
|
||||
instructionString = environment.strings.FaceScan_Instruction_Rotate
|
||||
case .tracking:
|
||||
self.frameView.update(state: .segments(self.completedAngles), transition: .spring(duration: 0.3))
|
||||
instructionString = environment.strings.FaceScan_Instruction_Rotate
|
||||
case .completed:
|
||||
self.frameView.update(state: .success, transition: .spring(duration: 0.3))
|
||||
instructionString = ""
|
||||
}
|
||||
|
||||
let instructionSize = self.instruction.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: instructionString, font: Font.semibold(20.0), textColor: .white)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 3,
|
||||
lineSpacing: 0.1
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let instructionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - instructionSize.width) / 2.0), y: 484.0), size: instructionSize)
|
||||
if let instructionView = self.instruction.view {
|
||||
if instructionView.superview == nil {
|
||||
self.addSubview(instructionView)
|
||||
}
|
||||
instructionView.frame = instructionFrame
|
||||
}
|
||||
|
||||
let cancelSize = self.cancel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.list.itemAccentColor))
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() as? FaceScanScreen else {
|
||||
return
|
||||
}
|
||||
controller.dismiss(animated: true)
|
||||
},
|
||||
animateScale: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let cancelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - cancelSize.width) / 2.0), y: availableSize.height - cancelSize.height - environment.safeInsets.bottom - 22.0), size: cancelSize)
|
||||
if let cancelView = self.cancel.view {
|
||||
if cancelView.superview == nil {
|
||||
self.addSubview(cancelView)
|
||||
}
|
||||
cancelView.frame = cancelFrame
|
||||
transition.setAlpha(view: cancelView, alpha: self.processState == .completed ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class FaceScanScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
fileprivate let completion: (Bool) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
availability: Signal<AgeVerificationAvailability, NoError>,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.completion = completion
|
||||
|
||||
super.init(context: context, component: FaceScanScreenComponent(
|
||||
context: context,
|
||||
availability: availability
|
||||
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
self.title = ""
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
self.navigationPresentation = .standaloneFlatModal
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.animateIn()
|
||||
}
|
||||
|
||||
private func animateIn() {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.view.clipsToBounds = true
|
||||
self.view.layer.cornerRadius = layout.deviceMetrics.screenCornerRadius
|
||||
|
||||
self.view.layer.animatePosition(from: CGPoint(x: self.view.layer.position.x, y: self.view.layer.position.y + self.view.layer.bounds.size.height), to: self.view.layer.position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.view.clipsToBounds = false
|
||||
})
|
||||
}
|
||||
|
||||
private func animateOut(completion: (() -> Void)? = nil) {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.view.clipsToBounds = true
|
||||
self.view.layer.cornerRadius = layout.deviceMetrics.screenCornerRadius
|
||||
|
||||
self.view.layer.animatePosition(from: self.view.layer.position, to: CGPoint(x: self.view.layer.position.x, y: self.view.layer.position.y + self.view.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
|
||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
if flag {
|
||||
self.animateOut(completion: {
|
||||
super.dismiss(animated: false, completion: completion)
|
||||
})
|
||||
} else {
|
||||
super.dismiss(animated: flag, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FaceScanScreenComponent.View {
|
||||
private func updateTrailingGuideLines(yaw: Double, pitch: Double) {
|
||||
let now = Date()
|
||||
|
||||
guard now.timeIntervalSince(lastGuideLineUpdate) >= guideLineUpdateInterval else {
|
||||
return
|
||||
}
|
||||
self.lastGuideLineUpdate = now
|
||||
|
||||
let radius: CGFloat = 128.0
|
||||
let center = CGPoint(x: self.bounds.width / 2, y: self.frameView.center.y)
|
||||
|
||||
let maxRotation: Double = 0.5
|
||||
let normalizedYaw = min(max(yaw / maxRotation, -1.0), 1.0) * 1.5
|
||||
let normalizedPitch = min(max(pitch / maxRotation, -1.0), 1.0) * 1.5
|
||||
|
||||
let yawChange = abs(yaw - lastGuideLineYaw)
|
||||
let pitchChange = abs(pitch - lastGuideLinePitch)
|
||||
|
||||
let rotationMagnitude = sqrt(yaw * yaw + pitch * pitch)
|
||||
if rotationMagnitude > 0.01 && (yawChange > angleThreshold || pitchChange > angleThreshold) {
|
||||
if abs(pitch) > 0.01 {
|
||||
createHorizontalGuideLine(center: center, radius: radius, curvature: normalizedPitch)
|
||||
}
|
||||
|
||||
if abs(yaw) > 0.01 {
|
||||
createVerticalGuideLine(center: center, radius: radius, curvature: normalizedYaw)
|
||||
}
|
||||
|
||||
lastGuideLineYaw = yaw
|
||||
lastGuideLinePitch = pitch
|
||||
}
|
||||
|
||||
cleanupOldGuideLines()
|
||||
}
|
||||
|
||||
private func createHorizontalGuideLine(center: CGPoint, radius: CGFloat, curvature: Double) {
|
||||
let lineLayer = CAShapeLayer()
|
||||
let path = createCurvedHorizontalPath(center: center, radius: radius, curvature: curvature)
|
||||
|
||||
lineLayer.path = path.cgPath
|
||||
lineLayer.strokeColor = UIColor(rgb: 0xb4f5ff).cgColor
|
||||
lineLayer.lineWidth = 3.0
|
||||
lineLayer.fillColor = UIColor.clear.cgColor
|
||||
lineLayer.lineCap = .round
|
||||
lineLayer.opacity = 0.3
|
||||
|
||||
lineLayer.shadowColor = UIColor(rgb: 0xb4f5ff).cgColor
|
||||
lineLayer.shadowRadius = 2
|
||||
lineLayer.shadowOpacity = 0.5
|
||||
lineLayer.shadowOffset = .zero
|
||||
|
||||
self.layer.addSublayer(lineLayer)
|
||||
horizontalGuideLines.append(lineLayer)
|
||||
|
||||
animateLinefadeOut(lineLayer)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + guideLineFadeDuration + 0.1) { [weak self] in
|
||||
if let index = self?.horizontalGuideLines.firstIndex(of: lineLayer) {
|
||||
self?.horizontalGuideLines.remove(at: index)
|
||||
}
|
||||
lineLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private func createVerticalGuideLine(center: CGPoint, radius: CGFloat, curvature: Double) {
|
||||
let lineLayer = CAShapeLayer()
|
||||
let path = createCurvedVerticalPath(center: center, radius: radius, curvature: curvature)
|
||||
|
||||
lineLayer.path = path.cgPath
|
||||
lineLayer.strokeColor = UIColor(rgb: 0xb4f5ff).cgColor
|
||||
lineLayer.lineWidth = 3.0
|
||||
lineLayer.fillColor = UIColor.clear.cgColor
|
||||
lineLayer.lineCap = .round
|
||||
lineLayer.opacity = 0.3
|
||||
|
||||
lineLayer.shadowColor = UIColor(rgb: 0xb4f5ff).cgColor
|
||||
lineLayer.shadowRadius = 2
|
||||
lineLayer.shadowOpacity = 0.5
|
||||
lineLayer.shadowOffset = .zero
|
||||
|
||||
self.layer.addSublayer(lineLayer)
|
||||
verticalGuideLines.append(lineLayer)
|
||||
|
||||
animateLinefadeOut(lineLayer)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + guideLineFadeDuration + 0.1) { [weak self] in
|
||||
if let index = self?.verticalGuideLines.firstIndex(of: lineLayer) {
|
||||
self?.verticalGuideLines.remove(at: index)
|
||||
}
|
||||
lineLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private func createCurvedHorizontalPath(center: CGPoint, radius: CGFloat, curvature: Double) -> UIBezierPath {
|
||||
let path = UIBezierPath()
|
||||
|
||||
let startPoint = CGPoint(x: center.x - radius, y: center.y)
|
||||
let endPoint = CGPoint(x: center.x + radius, y: center.y)
|
||||
|
||||
if abs(curvature) < 0.05 {
|
||||
path.move(to: startPoint)
|
||||
path.addLine(to: endPoint)
|
||||
} else {
|
||||
let curveOffset = CGFloat(curvature) * radius * 0.6
|
||||
let controlPoint = CGPoint(x: center.x, y: center.y + curveOffset)
|
||||
|
||||
path.move(to: startPoint)
|
||||
path.addQuadCurve(to: endPoint, controlPoint: controlPoint)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
private func createCurvedVerticalPath(center: CGPoint, radius: CGFloat, curvature: Double) -> UIBezierPath {
|
||||
let path = UIBezierPath()
|
||||
|
||||
let startPoint = CGPoint(x: center.x, y: center.y - radius)
|
||||
let endPoint = CGPoint(x: center.x, y: center.y + radius)
|
||||
|
||||
if abs(curvature) < 0.05 {
|
||||
path.move(to: startPoint)
|
||||
path.addLine(to: endPoint)
|
||||
} else {
|
||||
let curveOffset = CGFloat(curvature) * radius * 0.6
|
||||
let controlPoint = CGPoint(x: center.x + curveOffset, y: center.y)
|
||||
|
||||
path.move(to: startPoint)
|
||||
path.addQuadCurve(to: endPoint, controlPoint: controlPoint)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
private func animateLinefadeOut(_ layer: CAShapeLayer) {
|
||||
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
|
||||
opacityAnimation.fromValue = 0.5
|
||||
opacityAnimation.toValue = 0.0
|
||||
opacityAnimation.duration = guideLineFadeDuration
|
||||
opacityAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
opacityAnimation.fillMode = .forwards
|
||||
opacityAnimation.isRemovedOnCompletion = false
|
||||
|
||||
let shadowAnimation = CABasicAnimation(keyPath: "shadowOpacity")
|
||||
shadowAnimation.fromValue = 0.5
|
||||
shadowAnimation.toValue = 0.0
|
||||
shadowAnimation.duration = guideLineFadeDuration
|
||||
shadowAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
shadowAnimation.fillMode = .forwards
|
||||
shadowAnimation.isRemovedOnCompletion = false
|
||||
|
||||
layer.add(opacityAnimation, forKey: "fadeOut")
|
||||
layer.add(shadowAnimation, forKey: "shadowFadeOut")
|
||||
}
|
||||
|
||||
private func cleanupOldGuideLines() {
|
||||
while horizontalGuideLines.count > maxGuideLines {
|
||||
let oldestLine = horizontalGuideLines.removeFirst()
|
||||
oldestLine.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
while verticalGuideLines.count > maxGuideLines {
|
||||
let oldestLine = verticalGuideLines.removeFirst()
|
||||
oldestLine.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private func clearAllGuideLines() {
|
||||
for line in horizontalGuideLines {
|
||||
line.removeFromSuperlayer()
|
||||
}
|
||||
self.horizontalGuideLines.removeAll()
|
||||
|
||||
for line in verticalGuideLines {
|
||||
line.removeFromSuperlayer()
|
||||
}
|
||||
self.verticalGuideLines.removeAll()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
final class FrameView: UIView {
|
||||
enum State: Equatable {
|
||||
case viewFinder
|
||||
case segments(Set<Int>)
|
||||
case success
|
||||
case failure
|
||||
}
|
||||
|
||||
private let viewFinderLayer = ViewFinderLayer()
|
||||
private let transitionLayer = TransitionLayer()
|
||||
private let segmentsLayer = SegmentsLayer()
|
||||
|
||||
private var currentState: State = .viewFinder
|
||||
private var scheduledState: State?
|
||||
private var isTransitioning = false
|
||||
|
||||
private var currentLayout: CGSize?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.transitionLayer.isHidden = true
|
||||
self.segmentsLayer.isHidden = true
|
||||
|
||||
self.layer.addSublayer(self.viewFinderLayer)
|
||||
self.layer.addSublayer(self.transitionLayer)
|
||||
self.layer.addSublayer(self.segmentsLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(state: State, intermediateCompletion: (() -> Void)? = nil, transition: ComponentTransition) {
|
||||
guard !self.isTransitioning else {
|
||||
self.scheduledState = state
|
||||
return
|
||||
}
|
||||
|
||||
let previousState = self.currentState
|
||||
self.currentState = state
|
||||
|
||||
switch state {
|
||||
case .viewFinder:
|
||||
switch previousState {
|
||||
case .viewFinder:
|
||||
break
|
||||
case .segments:
|
||||
self.isTransitioning = true
|
||||
self.segmentsLayer.animateOut(transition: transition) {
|
||||
self.segmentsLayer.isHidden = true
|
||||
self.transitionLayer.isHidden = false
|
||||
self.transitionLayer.animateOut(transition: transition) {
|
||||
self.transitionLayer.isHidden = true
|
||||
self.viewFinderLayer.isHidden = false
|
||||
intermediateCompletion?()
|
||||
self.viewFinderLayer.animateIn(transition: transition) {
|
||||
self.isTransitioning = false
|
||||
self.maybeApplyScheduledState()
|
||||
}
|
||||
}
|
||||
}
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
case let .segments(segments):
|
||||
switch previousState {
|
||||
case .viewFinder:
|
||||
self.isTransitioning = true
|
||||
self.viewFinderLayer.animateOut(transition: transition) {
|
||||
self.viewFinderLayer.isHidden = true
|
||||
self.transitionLayer.isHidden = false
|
||||
self.transitionLayer.animateIn(transition: transition) {
|
||||
self.transitionLayer.isHidden = true
|
||||
self.segmentsLayer.isHidden = false
|
||||
self.segmentsLayer.animateIn (transition: transition) {
|
||||
self.isTransitioning = false
|
||||
self.maybeApplyScheduledState()
|
||||
}
|
||||
}
|
||||
}
|
||||
case .segments:
|
||||
self.segmentsLayer.update(segments: segments, transition: transition)
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
case .success:
|
||||
self.isTransitioning = true
|
||||
self.segmentsLayer.animateOut(transition: transition) {
|
||||
self.segmentsLayer.isHidden = true
|
||||
self.transitionLayer.isHidden = false
|
||||
self.transitionLayer.update(color: UIColor(rgb: 0x65c466))
|
||||
self.transitionLayer.animateOut(transition: transition) {
|
||||
self.isTransitioning = false
|
||||
self.maybeApplyScheduledState()
|
||||
}
|
||||
}
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func maybeApplyScheduledState() {
|
||||
if !self.isTransitioning, let state = self.scheduledState {
|
||||
self.scheduledState = nil
|
||||
self.update(state: state, transition: .spring(duration: 0.3))
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
guard self.currentLayout != size else {
|
||||
return
|
||||
}
|
||||
self.currentLayout = size
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
|
||||
//let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
//let viewFinderWidth = bounds.width - 34.0
|
||||
//let viewFinderSize = CGSize(width: viewFinderWidth, height: floor(viewFinderWidth * 1.17778))
|
||||
|
||||
let viewFinderFrame = bounds.insetBy(dx: 29.0, dy: 29.0) //viewFinderSize.centered(around: center)
|
||||
self.viewFinderLayer.update(size: viewFinderFrame.size, closed: self.currentState != .viewFinder, transition: .immediate)
|
||||
self.viewFinderLayer.frame = viewFinderFrame
|
||||
|
||||
let transitionFrame = bounds.insetBy(dx: 29.0, dy: 29.0) //viewFinderSize.centered(around: center)
|
||||
self.transitionLayer.update(size: transitionFrame.size)
|
||||
self.transitionLayer.frame = transitionFrame
|
||||
|
||||
let segmentsFrame = bounds.insetBy(dx: 15.0, dy: 15.0)
|
||||
self.segmentsLayer.update(size: segmentsFrame.size)
|
||||
self.segmentsLayer.frame = segmentsFrame
|
||||
}
|
||||
}
|
||||
|
||||
private let numberOfSegments = 64
|
||||
private let lineWidth: CGFloat = 4.0
|
||||
|
||||
final class ViewFinderLayer: SimpleLayer {
|
||||
private let viewFinderTopLeftLine = SimpleShapeLayer()
|
||||
private let viewFinderTopRightLine = SimpleShapeLayer()
|
||||
private let viewFinderBottomLeftLine = SimpleShapeLayer()
|
||||
private let viewFinderBottomRightLine = SimpleShapeLayer()
|
||||
|
||||
private var viewFinderLines: [SimpleShapeLayer] {
|
||||
return [
|
||||
self.viewFinderTopLeftLine,
|
||||
self.viewFinderTopRightLine,
|
||||
self.viewFinderBottomLeftLine,
|
||||
self.viewFinderBottomRightLine
|
||||
]
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
for line in self.viewFinderLines {
|
||||
line.strokeColor = UIColor.white.cgColor
|
||||
line.fillColor = UIColor.clear.cgColor
|
||||
line.lineWidth = lineWidth
|
||||
line.lineCap = .round
|
||||
self.addSublayer(line)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) {
|
||||
guard let size = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.update(size: size, closed: true, transition: transition, completion: completion)
|
||||
}
|
||||
|
||||
func animateIn(transition: ComponentTransition, completion: @escaping () -> Void) {
|
||||
guard let size = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.update(size: size, closed: false, transition: transition, completion: completion)
|
||||
}
|
||||
|
||||
func update(size: CGSize, closed: Bool, transition: ComponentTransition, completion: (() -> Void)? = nil) {
|
||||
self.validLayout = size
|
||||
|
||||
let cornerRadius = closed ? size.width / 2.0 : 18.0
|
||||
|
||||
let lineLength = size.width / 2.0 - cornerRadius
|
||||
let targetLineLength = 34.0
|
||||
let fraction = targetLineLength / lineLength
|
||||
let strokeFraction = (1.0 - fraction) / 2.0
|
||||
let strokeStart = closed ? 0.0 : strokeFraction
|
||||
let strokeEnd = closed ? 1.0 : 1.0 - strokeFraction
|
||||
|
||||
let topLeftPath = CGMutablePath()
|
||||
topLeftPath.move(to: CGPoint(x: 0.0, y: size.height / 2.0))
|
||||
topLeftPath.addArc(center: CGPoint(x: cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: -.pi, endAngle: -.pi / 2.0, clockwise: false)
|
||||
topLeftPath.addLine(to: CGPoint(x: size.width / 2.0, y: 0.0))
|
||||
|
||||
transition.setShapeLayerPath(layer: self.viewFinderTopLeftLine, path: topLeftPath, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
transition.setShapeLayerStrokeStart(layer: self.viewFinderTopLeftLine, strokeStart: strokeStart)
|
||||
transition.setShapeLayerStrokeEnd(layer: self.viewFinderTopLeftLine, strokeEnd: strokeEnd)
|
||||
|
||||
let topRightPath = CGMutablePath()
|
||||
topRightPath.move(to: CGPoint(x: size.width / 2.0, y: 0.0))
|
||||
topRightPath.addArc(center: CGPoint(x: size.width - cornerRadius, y: cornerRadius), radius: cornerRadius, startAngle: -.pi / 2.0, endAngle: 0.0, clockwise: false)
|
||||
topRightPath.addLine(to: CGPoint(x: size.width, y: size.height / 2.0))
|
||||
|
||||
transition.setShapeLayerPath(layer: self.viewFinderTopRightLine, path: topRightPath)
|
||||
transition.setShapeLayerStrokeStart(layer: self.viewFinderTopRightLine, strokeStart: strokeStart)
|
||||
transition.setShapeLayerStrokeEnd(layer: self.viewFinderTopRightLine, strokeEnd: strokeEnd)
|
||||
|
||||
let bottomRightPath = CGMutablePath()
|
||||
bottomRightPath.move(to: CGPoint(x: size.width, y: size.height / 2.0))
|
||||
bottomRightPath.addArc(center: CGPoint(x: size.width - cornerRadius, y: size.height - cornerRadius), radius: cornerRadius, startAngle: 0.0, endAngle: .pi / 2.0, clockwise: false)
|
||||
bottomRightPath.addLine(to: CGPoint(x: size.width / 2.0, y: size.height))
|
||||
|
||||
transition.setShapeLayerPath(layer: self.viewFinderBottomRightLine, path: bottomRightPath)
|
||||
transition.setShapeLayerStrokeStart(layer: self.viewFinderBottomRightLine, strokeStart: strokeStart)
|
||||
transition.setShapeLayerStrokeEnd(layer: self.viewFinderBottomRightLine, strokeEnd: strokeEnd)
|
||||
|
||||
let bottomLeftPath = CGMutablePath()
|
||||
bottomLeftPath.move(to: CGPoint(x: size.width / 2.0, y: size.height))
|
||||
bottomLeftPath.addArc(center: CGPoint(x: cornerRadius, y: size.height - cornerRadius), radius: cornerRadius, startAngle: .pi / 2.0, endAngle: .pi, clockwise: false)
|
||||
bottomLeftPath.addLine(to: CGPoint(x: 0.0, y: size.height / 2.0))
|
||||
|
||||
transition.setShapeLayerPath(layer: self.viewFinderBottomLeftLine, path: bottomLeftPath)
|
||||
transition.setShapeLayerStrokeStart(layer: self.viewFinderBottomLeftLine, strokeStart: strokeStart)
|
||||
transition.setShapeLayerStrokeEnd(layer: self.viewFinderBottomLeftLine, strokeEnd: strokeEnd)
|
||||
|
||||
for line in self.viewFinderLines {
|
||||
line.frame = CGRect(origin: .zero, size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
final class TransitionLayer: SimpleLayer {
|
||||
private var segmentLayers: [SimpleShapeLayer] = []
|
||||
|
||||
func animateIn(transition: ComponentTransition, completion: @escaping () -> Void) {
|
||||
var i = 0
|
||||
for layer in self.segmentLayers {
|
||||
transition.setShapeLayerStrokeStart(layer: layer, strokeStart: 0.499)
|
||||
transition.setShapeLayerStrokeEnd(layer: layer, strokeEnd: 0.501, completion: i == 0 ? { _ in completion() } : nil)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) {
|
||||
var i = 0
|
||||
for layer in self.segmentLayers {
|
||||
transition.setShapeLayerStrokeStart(layer: layer, strokeStart: 0.0)
|
||||
transition.setShapeLayerStrokeEnd(layer: layer, strokeEnd: 1.0, completion: i == 0 ? { _ in completion() } : nil)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
func setupIfNeeded(size: CGSize) {
|
||||
guard self.segmentLayers.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
let radius: CGFloat = size.width / 2.0
|
||||
let gapInDegrees: CGFloat = 0.0
|
||||
let gapInRadians: CGFloat = gapInDegrees * .pi / 180.0
|
||||
|
||||
let totalGapAngle = CGFloat(numberOfSegments) * gapInRadians
|
||||
let totalSegmentAngle = 2 * .pi - totalGapAngle
|
||||
let segmentAngle = totalSegmentAngle / CGFloat(numberOfSegments)
|
||||
|
||||
for i in 0 ..< numberOfSegments {
|
||||
let startAngle = -segmentAngle * 0.5 + (CGFloat(i) * (segmentAngle + gapInRadians)) - .pi / 2
|
||||
let endAngle = startAngle + segmentAngle
|
||||
|
||||
let path = UIBezierPath(arcCenter: center,
|
||||
radius: radius,
|
||||
startAngle: startAngle,
|
||||
endAngle: endAngle,
|
||||
clockwise: true)
|
||||
|
||||
let stripeLayer = SimpleShapeLayer()
|
||||
stripeLayer.path = path.cgPath
|
||||
stripeLayer.strokeColor = UIColor(rgb: 0xaaaaaa).cgColor
|
||||
stripeLayer.lineWidth = lineWidth
|
||||
stripeLayer.fillColor = UIColor.clear.cgColor
|
||||
stripeLayer.lineCap = .round
|
||||
|
||||
self.addSublayer(stripeLayer)
|
||||
self.segmentLayers.append(stripeLayer)
|
||||
}
|
||||
}
|
||||
|
||||
func update(color: UIColor) {
|
||||
for layer in self.segmentLayers {
|
||||
layer.strokeColor = color.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
self.setupIfNeeded(size: size)
|
||||
}
|
||||
}
|
||||
|
||||
final class SegmentsLayer: SimpleLayer {
|
||||
private var segmentLayers: [SimpleShapeLayer] = []
|
||||
|
||||
func animateIn(transition: ComponentTransition, completion: @escaping () -> Void) {
|
||||
var i = 0
|
||||
for layer in self.segmentLayers {
|
||||
transition.setShapeLayerStrokeStart(layer: layer, strokeStart: 0.0)
|
||||
transition.setShapeLayerStrokeEnd(layer: layer, strokeEnd: 0.32, completion: i == 0 ? { _ in completion() } : nil)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(transition: ComponentTransition, completion: @escaping () -> Void) {
|
||||
var i = 0
|
||||
for layer in self.segmentLayers {
|
||||
transition.setShapeLayerStrokeStart(layer: layer, strokeStart: 0.0)
|
||||
transition.setShapeLayerStrokeEnd(layer: layer, strokeEnd: 0.001, completion: i == 0 ? { _ in completion() } : nil)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
func setupIfNeeded(size: CGSize) {
|
||||
guard self.segmentLayers.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
let innerRadius: CGFloat = size.width / 2.0 - 13.0
|
||||
let outerRadius: CGFloat = size.width / 2.0 + 13.0
|
||||
let gapInDegrees: CGFloat = 2.0
|
||||
let gapInRadians: CGFloat = gapInDegrees * .pi / 180.0
|
||||
|
||||
let totalGapAngle = CGFloat(numberOfSegments) * gapInRadians
|
||||
let totalSegmentAngle = 2 * .pi - totalGapAngle
|
||||
let segmentAngle = totalSegmentAngle / CGFloat(numberOfSegments)
|
||||
|
||||
for i in 0 ..< numberOfSegments {
|
||||
let angle = (CGFloat(i) * (segmentAngle + gapInRadians)) - .pi / 2
|
||||
|
||||
let startPoint = CGPoint(
|
||||
x: center.x + innerRadius * cos(angle),
|
||||
y: center.y + innerRadius * sin(angle)
|
||||
)
|
||||
|
||||
let endPoint = CGPoint(
|
||||
x: center.x + outerRadius * cos(angle),
|
||||
y: center.y + outerRadius * sin(angle)
|
||||
)
|
||||
|
||||
let path = UIBezierPath()
|
||||
path.move(to: startPoint)
|
||||
path.addLine(to: endPoint)
|
||||
|
||||
let stripeLayer = SimpleShapeLayer()
|
||||
stripeLayer.path = path.cgPath
|
||||
stripeLayer.strokeColor = UIColor(rgb: 0xaaaaaa).cgColor
|
||||
stripeLayer.lineWidth = lineWidth
|
||||
stripeLayer.fillColor = UIColor.clear.cgColor
|
||||
stripeLayer.lineCap = .round
|
||||
stripeLayer.strokeStart = 0.0
|
||||
stripeLayer.strokeEnd = 0.001
|
||||
|
||||
self.addSublayer(stripeLayer)
|
||||
self.segmentLayers.append(stripeLayer)
|
||||
}
|
||||
}
|
||||
|
||||
func update(segments: Set<Int>, transition: ComponentTransition) {
|
||||
var mappedSegments = Set<Int>()
|
||||
for value in segments {
|
||||
for i in 0 ..< 4 {
|
||||
mappedSegments.insert(value * 4 + i)
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0 ..< numberOfSegments {
|
||||
let stripeLayer = self.segmentLayers[i]
|
||||
if mappedSegments.contains(i) {
|
||||
transition.setShapeLayerStrokeEnd(layer: stripeLayer, strokeEnd: 1.0)
|
||||
transition.setShapeLayerStrokeColor(layer: stripeLayer, color: UIColor(rgb: 0x00ca48))
|
||||
} else {
|
||||
transition.setShapeLayerStrokeEnd(layer: stripeLayer, strokeEnd: 0.32)
|
||||
transition.setShapeLayerStrokeColor(layer: stripeLayer, color: UIColor(rgb: 0xaaaaaa))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
self.setupIfNeeded(size: size)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ swift_library(
|
|||
"//submodules/TextFormat",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent",
|
||||
|
|
|
|||
|
|
@ -460,9 +460,9 @@ final class GiftOptionsScreenComponent: Component {
|
|||
}
|
||||
if case let .generic(gift) = gift {
|
||||
if let perUserLimit = gift.perUserLimit, perUserLimit.remains == 0 {
|
||||
//TODO:localize
|
||||
let text = environment.strings.Gift_Options_Gift_BuyLimitReached(perUserLimit.total)
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: gift.file, loop: true, title: nil, text: "You've already sent \(perUserLimit.total) of these gifts, and it's the limit.", undoText: nil, customAction: nil), action: { _ in return false })
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: gift.file, loop: true, title: nil, text: text, undoText: nil, customAction: nil), action: { _ in return false })
|
||||
mainController.present(controller, in: .current)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -388,6 +388,7 @@ final class PeerInfoScreenData {
|
|||
let revenueStatsState: StarsRevenueStats?
|
||||
let revenueStatsContext: StarsRevenueStatsContext?
|
||||
let profileGiftsContext: ProfileGiftsContext?
|
||||
let profileGiftsCollectionsContext: ProfileGiftsCollectionsContext?
|
||||
let premiumGiftOptions: [PremiumGiftCodeOption]
|
||||
let webAppPermissions: WebAppPermissionsState?
|
||||
|
||||
|
|
@ -440,6 +441,7 @@ final class PeerInfoScreenData {
|
|||
revenueStatsState: StarsRevenueStats?,
|
||||
revenueStatsContext: StarsRevenueStatsContext?,
|
||||
profileGiftsContext: ProfileGiftsContext?,
|
||||
profileGiftsCollectionsContext: ProfileGiftsCollectionsContext?,
|
||||
premiumGiftOptions: [PremiumGiftCodeOption],
|
||||
webAppPermissions: WebAppPermissionsState?
|
||||
) {
|
||||
|
|
@ -480,6 +482,7 @@ final class PeerInfoScreenData {
|
|||
self.revenueStatsState = revenueStatsState
|
||||
self.revenueStatsContext = revenueStatsContext
|
||||
self.profileGiftsContext = profileGiftsContext
|
||||
self.profileGiftsCollectionsContext = profileGiftsCollectionsContext
|
||||
self.premiumGiftOptions = premiumGiftOptions
|
||||
self.webAppPermissions = webAppPermissions
|
||||
}
|
||||
|
|
@ -1000,13 +1003,14 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
|||
revenueStatsState: nil,
|
||||
revenueStatsContext: nil,
|
||||
profileGiftsContext: profileGiftsContext,
|
||||
profileGiftsCollectionsContext: nil,
|
||||
premiumGiftOptions: [],
|
||||
webAppPermissions: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, existingRequestsContext: PeerInvitationImportersContext?, existingProfileGiftsContext: ProfileGiftsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, privacySettings: Signal<AccountPrivacySettings?, NoError>, forceHasGifts: Bool) -> Signal<PeerInfoScreenData, NoError> {
|
||||
func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, existingRequestsContext: PeerInvitationImportersContext?, existingProfileGiftsContext: ProfileGiftsContext?, existingProfileGiftsCollectionsContext: ProfileGiftsCollectionsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, privacySettings: Signal<AccountPrivacySettings?, NoError>, forceHasGifts: Bool) -> Signal<PeerInfoScreenData, NoError> {
|
||||
return peerInfoScreenInputData(context: context, peerId: peerId, isSettings: isSettings)
|
||||
|> mapToSignal { inputData -> Signal<PeerInfoScreenData, NoError> in
|
||||
let wasUpgradedGroup = Atomic<Bool?>(value: nil)
|
||||
|
|
@ -1051,6 +1055,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
|||
revenueStatsState: nil,
|
||||
revenueStatsContext: nil,
|
||||
profileGiftsContext: nil,
|
||||
profileGiftsCollectionsContext: nil,
|
||||
premiumGiftOptions: [],
|
||||
webAppPermissions: nil
|
||||
))
|
||||
|
|
@ -1073,11 +1078,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
|||
|
||||
let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError>
|
||||
let profileGiftsContext: ProfileGiftsContext?
|
||||
let profileGiftsCollectionsContext: ProfileGiftsCollectionsContext?
|
||||
if case .user = kind {
|
||||
if isMyProfile || userPeerId != context.account.peerId {
|
||||
profileGiftsContext = existingProfileGiftsContext ?? ProfileGiftsContext(account: context.account, peerId: userPeerId)
|
||||
profileGiftsCollectionsContext = existingProfileGiftsCollectionsContext ?? ProfileGiftsCollectionsContext(account: context.account, peerId: userPeerId)
|
||||
} else {
|
||||
profileGiftsContext = nil
|
||||
profileGiftsCollectionsContext = nil
|
||||
}
|
||||
premiumGiftOptions = .single([])
|
||||
|> then(
|
||||
|
|
@ -1085,6 +1093,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
|||
)
|
||||
} else {
|
||||
profileGiftsContext = nil
|
||||
profileGiftsCollectionsContext = nil
|
||||
premiumGiftOptions = .single([])
|
||||
}
|
||||
|
||||
|
|
@ -1511,6 +1520,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
|||
revenueStatsState: revenueContextAndState.1,
|
||||
revenueStatsContext: revenueContextAndState.0,
|
||||
profileGiftsContext: profileGiftsContext,
|
||||
profileGiftsCollectionsContext: profileGiftsCollectionsContext,
|
||||
premiumGiftOptions: premiumGiftOptions,
|
||||
webAppPermissions: webAppPermissions
|
||||
)
|
||||
|
|
@ -1619,6 +1629,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
|||
}
|
||||
|
||||
let profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: peerId)
|
||||
let profileGiftsCollectionsContext = ProfileGiftsCollectionsContext(account: context.account, peerId: peerId)
|
||||
|
||||
let personalChannel = peerInfoPersonalOrLinkedChannel(context: context, peerId: peerId, isSettings: false)
|
||||
|
||||
|
|
@ -1743,6 +1754,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
|||
revenueStatsState: revenueContextAndState.1,
|
||||
revenueStatsContext: revenueContextAndState.0,
|
||||
profileGiftsContext: profileGiftsContext,
|
||||
profileGiftsCollectionsContext: profileGiftsCollectionsContext,
|
||||
premiumGiftOptions: [],
|
||||
webAppPermissions: nil
|
||||
)
|
||||
|
|
@ -2076,6 +2088,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
|||
revenueStatsState: nil,
|
||||
revenueStatsContext: nil,
|
||||
profileGiftsContext: nil,
|
||||
profileGiftsCollectionsContext: nil,
|
||||
premiumGiftOptions: [],
|
||||
webAppPermissions: nil
|
||||
))
|
||||
|
|
|
|||
|
|
@ -576,7 +576,7 @@ private final class PeerInfoPendingPane {
|
|||
}
|
||||
}
|
||||
}
|
||||
paneNode = PeerInfoGiftsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, profileGifts: data.profileGiftsContext!, canManage: canManage, canGift: canGift)
|
||||
paneNode = PeerInfoGiftsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, profileGiftsCollections: data.profileGiftsCollectionsContext!, profileGifts: data.profileGiftsContext!, canManage: canManage, canGift: canGift)
|
||||
case .stories, .storyArchive, .botPreview:
|
||||
var canManage = false
|
||||
if let peer = data.peer {
|
||||
|
|
|
|||
|
|
@ -4801,7 +4801,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
self.privacySettings.set(.single(nil))
|
||||
}
|
||||
|
||||
screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: hintGroupInCommon, existingRequestsContext: requestsContext, existingProfileGiftsContext: profileGiftsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic, privacySettings: self.privacySettings.get(), forceHasGifts: initialPaneKey == .gifts)
|
||||
screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: hintGroupInCommon, existingRequestsContext: requestsContext, existingProfileGiftsContext: profileGiftsContext, existingProfileGiftsCollectionsContext: nil, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic, privacySettings: self.privacySettings.get(), forceHasGifts: initialPaneKey == .gifts)
|
||||
|
||||
var previousTimestamp: Double?
|
||||
self.headerNode.displayPremiumIntro = { [weak self] sourceView, peerStatus, emojiStatusFileAndPack, white in
|
||||
|
|
@ -8863,6 +8863,73 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
self.controller?.push(SecretChatKeyController(context: self.context, fingerprint: encryptionKeyFingerprint, peer: EnginePeer(peer)))
|
||||
}
|
||||
|
||||
private func openShareLink(url: String) {
|
||||
let shareController = ShareController(context: self.context, subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData)
|
||||
shareController.completed = { [weak self] peerIds in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = (strongSelf.context.engine.data.get(
|
||||
EngineDataList(
|
||||
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
||||
)
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let peers = peerList.compactMap { $0 }
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let text: String
|
||||
var savedMessages = false
|
||||
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
|
||||
text = presentationData.strings.UserInfo_LinkForwardTooltip_SavedMessages_One
|
||||
savedMessages = true
|
||||
} else {
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).string
|
||||
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
||||
let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
|
||||
} else if let peer = peers.first {
|
||||
let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
|
||||
} else {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in
|
||||
if savedMessages, let self, action == .info {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
guard let navigationController = self.controller?.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true))
|
||||
})
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
})
|
||||
}
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
}
|
||||
self.view.endEditing(true)
|
||||
self.controller?.present(shareController, in: .window(.root))
|
||||
}
|
||||
|
||||
private func openShareBot() {
|
||||
let _ = (getUserPeer(engine: self.context.engine, peerId: self.peerId)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||
|
|
@ -8870,70 +8937,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
return
|
||||
}
|
||||
if case let .user(peer) = peer, let username = peer.addressName {
|
||||
let shareController = ShareController(context: strongSelf.context, subject: .url("https://t.me/\(username)"), updatedPresentationData: strongSelf.controller?.updatedPresentationData)
|
||||
shareController.completed = { [weak self] peerIds in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = (strongSelf.context.engine.data.get(
|
||||
EngineDataList(
|
||||
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
||||
)
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let peers = peerList.compactMap { $0 }
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let text: String
|
||||
var savedMessages = false
|
||||
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
|
||||
text = presentationData.strings.UserInfo_LinkForwardTooltip_SavedMessages_One
|
||||
savedMessages = true
|
||||
} else {
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).string
|
||||
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
||||
let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
|
||||
} else if let peer = peers.first {
|
||||
let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
|
||||
} else {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in
|
||||
if savedMessages, let self, action == .info {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
guard let navigationController = self.controller?.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true))
|
||||
})
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
})
|
||||
}
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
}
|
||||
strongSelf.view.endEditing(true)
|
||||
strongSelf.controller?.present(shareController, in: .window(.root))
|
||||
strongSelf.openShareLink(url: "https://t.me/\(username)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -11326,16 +11330,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
guard let data = self.data, let giftsContext = data.profileGiftsContext else {
|
||||
guard let data = self.data else {
|
||||
return
|
||||
}
|
||||
|
||||
let giftsContext = pane.giftsContext
|
||||
|
||||
var hasVisibility = false
|
||||
if let channel = data.peer as? TelegramChannel, channel.hasPermission(.sendSomething) {
|
||||
hasVisibility = true
|
||||
} else if data.peer?.id == self.context.account.peerId {
|
||||
hasVisibility = true
|
||||
}
|
||||
|
||||
let isCollection = giftsContext.collectionId != nil
|
||||
|
||||
let strings = self.presentationData.strings
|
||||
let items: Signal<ContextController.Items, NoError> = giftsContext.state
|
||||
|
|
@ -11347,16 +11355,76 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
break
|
||||
}
|
||||
}
|
||||
return (state.filter, state.sorting, hasPinnedGifts)
|
||||
return (state.filter, state.sorting, hasPinnedGifts || isCollection)
|
||||
}
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in
|
||||
let filterEquals = lhs.0 == rhs.0
|
||||
let sortingEquals = lhs.1 == rhs.1
|
||||
let hasPinnedGiftsEquals = lhs.2 == rhs.2
|
||||
return filterEquals && sortingEquals && hasPinnedGiftsEquals
|
||||
let canReorderEquals = lhs.2 == rhs.2
|
||||
return filterEquals && sortingEquals && canReorderEquals
|
||||
})
|
||||
|> map { [weak giftsContext] filter, sorting, hasPinnedGifts -> ContextController.Items in
|
||||
|> map { [weak self, weak pane, weak giftsContext] filter, sorting, canReorder -> ContextController.Items in
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if hasVisibility {
|
||||
if let pane, case .all = pane.currentCollection {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_AddCollection, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/Gifts/AddCollection"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak pane] _, f in
|
||||
f(.default)
|
||||
|
||||
if let pane {
|
||||
pane.createCollection()
|
||||
}
|
||||
})))
|
||||
} else {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_AddGifts, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/Gifts/AddGift"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak pane] _, f in
|
||||
f(.default)
|
||||
|
||||
if let pane, case let .collection(id) = pane.currentCollection {
|
||||
pane.addGiftsToCollection(id: id)
|
||||
}
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_ShareCollection, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
//TODO:release
|
||||
self?.openShareLink(url: "https://t.me/")
|
||||
})))
|
||||
}
|
||||
|
||||
if canReorder {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Reorder, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak pane] _, f in
|
||||
f(.default)
|
||||
|
||||
if let pane {
|
||||
pane.beginReordering()
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
if let pane, case let .collection(id) = pane.currentCollection {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_DeleteCollection, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||
}, action: { [weak pane] _, f in
|
||||
f(.default)
|
||||
|
||||
if let pane {
|
||||
pane.deleteCollection(id: id)
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
if !items.isEmpty {
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: sorting == .date ? strings.PeerInfo_Gifts_SortByValue : strings.PeerInfo_Gifts_SortByDate, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: sorting == .date ? "Peer Info/SortValue" : "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
|
||||
|
|
@ -11366,16 +11434,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
giftsContext?.updateSorting(sorting == .date ? .value : .date)
|
||||
})))
|
||||
|
||||
if hasPinnedGifts && hasVisibility {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Reorder, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
pane.beginReordering()
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
let toggleFilter: (ProfileGiftsContext.Filters) -> Void = { [weak giftsContext] value in
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ swift_library(
|
|||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||
"//submodules/InvisibleInkDustNode",
|
||||
"//submodules/MediaPickerUI",
|
||||
|
|
@ -56,6 +57,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
|
||||
"//submodules/PromptUI",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,501 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import BundleIconComponent
|
||||
import MultilineTextComponent
|
||||
import ButtonComponent
|
||||
import BlurredBackgroundComponent
|
||||
import ContextUI
|
||||
|
||||
final class AddGiftsScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let peerId: EnginePeer.Id
|
||||
let collectionId: Int32
|
||||
let profileGifts: ProfileGiftsContext
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
collectionId: Int32,
|
||||
profileGifts: ProfileGiftsContext
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.collectionId = collectionId
|
||||
self.profileGifts = profileGifts
|
||||
}
|
||||
|
||||
static func ==(lhs: AddGiftsScreenComponent, rhs: AddGiftsScreenComponent) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let backgroundView: UIView
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private var giftsListView: GiftsListView?
|
||||
|
||||
private let buttonBackground = ComponentView<Empty>()
|
||||
private let buttonSeparator = SimpleLayer()
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: AddGiftsScreenComponent?
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = UIView()
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let environment = self.environment, let giftsListView = self.giftsListView else {
|
||||
return
|
||||
}
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
|
||||
let contentHeight = giftsListView.updateScrolling(topInset: environment.navigationHeight + 10.0, visibleBounds: visibleBounds, transition: transition)
|
||||
|
||||
var contentSize = CGSize(width: self.scrollView.bounds.width, height: contentHeight)
|
||||
contentSize.height += environment.safeInsets.bottom
|
||||
contentSize.height = max(contentSize.height, self.scrollView.bounds.size.height)
|
||||
transition.setFrame(view: giftsListView, frame: CGRect(origin: CGPoint(), size: contentSize))
|
||||
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AddGiftsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let giftsListView: GiftsListView
|
||||
if let current = self.giftsListView {
|
||||
giftsListView = current
|
||||
} else {
|
||||
giftsListView = GiftsListView(context: component.context, peerId: component.peerId, profileGifts: component.profileGifts, giftsCollections: nil, canSelect: true, ignoreCollection: component.collectionId)
|
||||
giftsListView.selectionUpdated = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
self.scrollView.addSubview(giftsListView)
|
||||
self.giftsListView = giftsListView
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
self.environment = environment
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let buttonHeight: CGFloat = 50.0
|
||||
let bottomPanelPadding: CGFloat = 12.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
||||
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
|
||||
|
||||
let bottomPanelOffset: CGFloat = giftsListView.selectedItems.count > 0 ? 0.0 : bottomPanelHeight
|
||||
|
||||
let buttonString = environment.strings.AddGifts_AddGifts(Int32(giftsListView.selectedItems.count))
|
||||
let bottomPanelSize = self.buttonBackground.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BlurredBackgroundComponent(
|
||||
color: environment.theme.rootController.tabBar.backgroundColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight)
|
||||
)
|
||||
self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor
|
||||
|
||||
if let view = self.buttonBackground.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
self.layer.addSublayer(self.buttonSeparator)
|
||||
}
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: bottomPanelSize))
|
||||
transition.setFrame(layer: self.buttonSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
|
||||
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
let buttonSize = self.button.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: environment.theme.list.itemCheckColors.fillColor,
|
||||
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
cornerRadius: 10.0
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(buttonAttributedString.string),
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
|
||||
),
|
||||
action: { [weak self] in
|
||||
guard let self, let controller = self.environment?.controller() as? AddGiftsScreen, let giftsListView = self.giftsListView else {
|
||||
return
|
||||
}
|
||||
controller.completion(giftsListView.selectedItems)
|
||||
controller.dismiss(animated: true)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight)
|
||||
)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding + bottomPanelOffset), size: buttonSize))
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let _ = giftsListView.update(size: availableSize, sideInset: 0.0, bottomInset: max(environment.safeInsets.bottom, bottomPanelHeight), deviceMetrics: environment.deviceMetrics, visibleHeight: availableSize.height, isScrollingLockedAtTop: false, expandProgress: 0.0, presentationData: presentationData, synchronous: false, visibleBounds: visibleBounds, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: availableSize))
|
||||
self.backgroundView.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: availableSize))
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let r = super.hitTest(point, with: event)
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class AddGiftsScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
private let peerId: EnginePeer.Id
|
||||
private let collectionId: Int32
|
||||
fileprivate let completion: ([ProfileGiftsContext.State.StarGift]) -> Void
|
||||
|
||||
private let profileGifts: ProfileGiftsContext
|
||||
|
||||
private let filterButton: FilterHeaderButton
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
collectionId: Int32,
|
||||
completion: @escaping ([ProfileGiftsContext.State.StarGift]) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.collectionId = collectionId
|
||||
self.completion = completion
|
||||
|
||||
self.profileGifts = ProfileGiftsContext(account: context.account, peerId: peerId)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.filterButton = FilterHeaderButton(presentationData: presentationData)
|
||||
|
||||
super.init(context: context, component: AddGiftsScreenComponent(
|
||||
context: context,
|
||||
peerId: peerId,
|
||||
collectionId: collectionId,
|
||||
profileGifts: self.profileGifts
|
||||
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
|
||||
self.title = presentationData.strings.AddGifts_Title
|
||||
self.navigationPresentation = .modal
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? AddGiftsScreenComponent.View else {
|
||||
return
|
||||
}
|
||||
componentView.scrollToTop()
|
||||
}
|
||||
|
||||
self.filterButton.contextAction = { [weak self] sourceNode, gesture in
|
||||
self?.presentContextMenu(sourceView: sourceNode.view, gesture: gesture)
|
||||
}
|
||||
self.filterButton.addTarget(self, action: #selector(self.filterPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.filterButton)
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func presentContextMenu(sourceView: UIView, gesture: ContextGesture?) {
|
||||
let giftsContext = self.profileGifts
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
let items: Signal<ContextController.Items, NoError> = giftsContext.state
|
||||
|> map { state in
|
||||
var hasPinnedGifts = false
|
||||
for gift in state.gifts {
|
||||
if gift.pinnedToTop {
|
||||
hasPinnedGifts = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return (state.filter, state.sorting, hasPinnedGifts)
|
||||
}
|
||||
|> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in
|
||||
let filterEquals = lhs.0 == rhs.0
|
||||
let sortingEquals = lhs.1 == rhs.1
|
||||
let hasPinnedGiftsEquals = lhs.2 == rhs.2
|
||||
return filterEquals && sortingEquals && hasPinnedGiftsEquals
|
||||
})
|
||||
|> map { [weak giftsContext] filter, sorting, hasPinnedGifts -> ContextController.Items in
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: sorting == .date ? strings.PeerInfo_Gifts_SortByValue : strings.PeerInfo_Gifts_SortByDate, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: sorting == .date ? "Peer Info/SortValue" : "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak giftsContext] _, f in
|
||||
f(.default)
|
||||
|
||||
giftsContext?.updateSorting(sorting == .date ? .value : .date)
|
||||
})))
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
let toggleFilter: (ProfileGiftsContext.Filters) -> Void = { [weak giftsContext] value in
|
||||
var updatedFilter = filter
|
||||
if updatedFilter.contains(value) {
|
||||
updatedFilter.remove(value)
|
||||
} else {
|
||||
updatedFilter.insert(value)
|
||||
}
|
||||
if !updatedFilter.contains(.unlimited) && !updatedFilter.contains(.limited) && !updatedFilter.contains(.unique) {
|
||||
updatedFilter.insert(.unlimited)
|
||||
}
|
||||
if !updatedFilter.contains(.displayed) && !updatedFilter.contains(.hidden) {
|
||||
if value == .displayed {
|
||||
updatedFilter.insert(.hidden)
|
||||
} else {
|
||||
updatedFilter.insert(.displayed)
|
||||
}
|
||||
}
|
||||
giftsContext?.updateFilter(updatedFilter)
|
||||
}
|
||||
|
||||
let switchToFilter: (ProfileGiftsContext.Filters) -> Void = { [weak giftsContext] value in
|
||||
var updatedFilter = filter
|
||||
updatedFilter.remove(.unlimited)
|
||||
updatedFilter.remove(.limited)
|
||||
updatedFilter.remove(.unique)
|
||||
updatedFilter.insert(value)
|
||||
giftsContext?.updateFilter(updatedFilter)
|
||||
}
|
||||
|
||||
let switchToVisiblityFilter: (ProfileGiftsContext.Filters) -> Void = { [weak giftsContext] value in
|
||||
var updatedFilter = filter
|
||||
updatedFilter.remove(.hidden)
|
||||
updatedFilter.remove(.displayed)
|
||||
updatedFilter.insert(value)
|
||||
giftsContext?.updateFilter(updatedFilter)
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Unlimited, icon: { theme in
|
||||
return filter.contains(.unlimited) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, f in
|
||||
toggleFilter(.unlimited)
|
||||
}, longPressAction: { _, f in
|
||||
switchToFilter(.unlimited)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Limited, icon: { theme in
|
||||
return filter.contains(.limited) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, f in
|
||||
toggleFilter(.limited)
|
||||
}, longPressAction: { _, f in
|
||||
switchToFilter(.limited)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Unique, icon: { theme in
|
||||
return filter.contains(.unique) ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
}, action: { _, f in
|
||||
toggleFilter(.unique)
|
||||
}, longPressAction: { _, f in
|
||||
switchToFilter(.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)
|
||||
}, longPressAction: { _, f in
|
||||
switchToVisiblityFilter(.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)
|
||||
}, longPressAction: { _, f in
|
||||
switchToVisiblityFilter(.hidden)
|
||||
})))
|
||||
|
||||
return ContextController.Items(content: .list(items))
|
||||
}
|
||||
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: items, gesture: gesture)
|
||||
self.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
@objc private func filterPressed() {
|
||||
self.filterButton.contextAction?(self.filterButton.containerNode, nil)
|
||||
}
|
||||
}
|
||||
|
||||
private final class FilterHeaderButton: HighlightableButtonNode {
|
||||
let referenceNode: ContextReferenceContentNode
|
||||
let containerNode: ContextControllerSourceNode
|
||||
private let icon = ComponentView<Empty>()
|
||||
|
||||
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||
|
||||
init(presentationData: PresentationData) {
|
||||
self.referenceNode = ContextReferenceContentNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.containerNode.animateScale = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.containerNode.addSubnode(self.referenceNode)
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.containerNode.shouldBegin = { [weak self] location in
|
||||
guard let strongSelf = self, let _ = strongSelf.contextAction else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
self.containerNode.activated = { [weak self] gesture, _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.contextAction?(strongSelf.containerNode, gesture)
|
||||
}
|
||||
|
||||
self.update(theme: presentationData.theme, strings: presentationData.strings)
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Peer Info/SortIcon",
|
||||
tintColor: theme.rootController.navigationBar.accentTextColor
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
if let view = self.icon.view {
|
||||
if view.superview == nil {
|
||||
view.isUserInteractionEnabled = false
|
||||
self.referenceNode.view.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: 14.0, y: 7.0), size: iconSize)
|
||||
}
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0))
|
||||
self.referenceNode.frame = self.containerNode.bounds
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
return CGSize(width: 44.0, height: 44.0)
|
||||
}
|
||||
|
||||
func onLayout() {
|
||||
}
|
||||
}
|
||||
|
||||
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
|
||||
private let controller: ViewController
|
||||
private let sourceView: UIView
|
||||
|
||||
init(controller: ViewController, sourceView: UIView) {
|
||||
self.controller = controller
|
||||
self.sourceView = sourceView
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -598,9 +598,11 @@ public class ShareRootControllerImpl {
|
|||
}
|
||||
|
||||
var canShareToStory = true
|
||||
var canSendInHighQuality = false
|
||||
if let inputItems = self?.getExtensionContext()?.inputItems, inputItems.count == 1, let item = inputItems[0] as? NSExtensionItem, let attachments = item.attachments {
|
||||
for attachment in attachments {
|
||||
if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
|
||||
canSendInHighQuality = true
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
|
||||
} else {
|
||||
canShareToStory = false
|
||||
|
|
@ -609,6 +611,7 @@ public class ShareRootControllerImpl {
|
|||
}
|
||||
|
||||
if canShareToStory {
|
||||
shareController.canSendInHighQuality = canSendInHighQuality
|
||||
shareController.shareStory = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ final class StarsOverviewItemComponent: Component {
|
|||
let theme: PresentationTheme
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let title: String
|
||||
let value: StarsAmount
|
||||
let value: CurrencyAmount
|
||||
let rate: Double
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
dateTimeFormat: PresentationDateTimeFormat,
|
||||
title: String,
|
||||
value: StarsAmount,
|
||||
value: CurrencyAmount,
|
||||
rate: Double
|
||||
) {
|
||||
self.theme = theme
|
||||
|
|
@ -60,9 +60,7 @@ final class StarsOverviewItemComponent: Component {
|
|||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.icon.image = UIImage(bundleImageName: "Premium/Stars/StarMedium")
|
||||
|
||||
|
||||
self.addSubview(self.icon)
|
||||
}
|
||||
|
||||
|
|
@ -75,14 +73,24 @@ final class StarsOverviewItemComponent: Component {
|
|||
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
let iconY: CGFloat = component.value.currency == .ton ? 13.0 + UIScreenPixel : 10.0
|
||||
if self.icon.image == nil {
|
||||
switch component.value.currency {
|
||||
case .ton:
|
||||
self.icon.image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: component.theme.list.itemAccentColor)
|
||||
case .stars:
|
||||
self.icon.image = UIImage(bundleImageName: "Premium/Stars/StarMedium")
|
||||
}
|
||||
}
|
||||
|
||||
var valueOffset: CGFloat = 0.0
|
||||
if let icon = self.icon.image {
|
||||
self.icon.frame = CGRect(origin: CGPoint(x: sideInset - 1.0, y: 10.0), size: icon.size)
|
||||
self.icon.frame = CGRect(origin: CGPoint(x: sideInset - 1.0, y: iconY), size: icon.size)
|
||||
valueOffset += icon.size.width
|
||||
}
|
||||
|
||||
let valueString = formatStarsAmountText(component.value, dateTimeFormat: component.dateTimeFormat)
|
||||
let usdValueString = formatTonUsdValue(component.value.value, divide: false, rate: component.rate, dateTimeFormat: component.dateTimeFormat)
|
||||
let valueString = formatStarsAmountText(component.value.amount, dateTimeFormat: component.dateTimeFormat)
|
||||
let usdValueString = formatTonUsdValue(component.value.amount.value, divide: false, rate: component.rate, dateTimeFormat: component.dateTimeFormat)
|
||||
|
||||
let valueSize = self.value.update(
|
||||
transition: .immediate,
|
||||
|
|
|
|||
|
|
@ -522,21 +522,21 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
theme: environment.theme,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
title: strings.Stars_BotRevenue_Proceeds_Available,
|
||||
value: starsState?.balances.availableBalance.amount ?? StarsAmount.zero,
|
||||
value: starsState?.balances.availableBalance ?? CurrencyAmount(amount: .zero, currency: .stars),
|
||||
rate: starsState?.usdRate ?? 0.0
|
||||
))),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent(
|
||||
theme: environment.theme,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
title: strings.Stars_BotRevenue_Proceeds_Current,
|
||||
value: starsState?.balances.currentBalance.amount ?? StarsAmount.zero,
|
||||
value: starsState?.balances.currentBalance ?? CurrencyAmount(amount: .zero, currency: .stars),
|
||||
rate: starsState?.usdRate ?? 0.0
|
||||
))),
|
||||
AnyComponentWithIdentity(id: 2, component: AnyComponent(StarsOverviewItemComponent(
|
||||
theme: environment.theme,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
title: strings.Stars_BotRevenue_Proceeds_Total,
|
||||
value: starsState?.balances.overallRevenue.amount ?? StarsAmount.zero,
|
||||
value: starsState?.balances.overallRevenue ?? CurrencyAmount(amount: .zero, currency: .stars),
|
||||
rate: starsState?.usdRate ?? 0.0
|
||||
)))
|
||||
],
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ final class StarsTransactionsScreenComponent: Component {
|
|||
private let titleView = ComponentView<Empty>()
|
||||
private let descriptionView = ComponentView<Empty>()
|
||||
|
||||
private let proceedsView = ComponentView<Empty>()
|
||||
private let balanceView = ComponentView<Empty>()
|
||||
private let earnStarsSection = ComponentView<Empty>()
|
||||
|
||||
|
|
@ -154,6 +155,8 @@ final class StarsTransactionsScreenComponent: Component {
|
|||
private var allTransactionsContext: StarsTransactionsContext?
|
||||
private var incomingTransactionsContext: StarsTransactionsContext?
|
||||
private var outgoingTransactionsContext: StarsTransactionsContext?
|
||||
|
||||
private var cachedChevronImage: (UIImage, PresentationTheme)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
|
||||
|
|
@ -667,14 +670,93 @@ final class StarsTransactionsScreenComponent: Component {
|
|||
contentHeight += 29.0
|
||||
|
||||
let withdrawAvailable = (self.revenueState?.balances.overallRevenue.amount.value ?? 0) > 0
|
||||
|
||||
|
||||
// if component.starsContext.ton {
|
||||
// //TODO:localize
|
||||
// let proceedsSize = self.proceedsView.update(
|
||||
// transition: .immediate,
|
||||
// component: AnyComponent(ListSectionComponent(
|
||||
// theme: environment.theme,
|
||||
// header: AnyComponent(MultilineTextComponent(
|
||||
// text: .plain(NSAttributedString(
|
||||
// string: "Proceeds Overview".uppercased(),
|
||||
// font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
// textColor: environment.theme.list.freeTextColor
|
||||
// )),
|
||||
// maximumNumberOfLines: 0
|
||||
// )),
|
||||
// footer: nil,
|
||||
// items: [
|
||||
// AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsOverviewItemComponent(
|
||||
// theme: environment.theme,
|
||||
// dateTimeFormat: environment.dateTimeFormat,
|
||||
// title: "Balance Available to Withdraw",
|
||||
// value: self.revenueState?.balances.availableBalance ?? CurrencyAmount(amount: .zero, currency: .stars),
|
||||
// rate: self.revenueState?.usdRate ?? 0.0
|
||||
// ))),
|
||||
// AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent(
|
||||
// theme: environment.theme,
|
||||
// dateTimeFormat: environment.dateTimeFormat,
|
||||
// title: "Total Lifetime Proceeds",
|
||||
// value: self.revenueState?.balances.overallRevenue ?? CurrencyAmount(amount: .zero, currency: .stars),
|
||||
// rate: self.revenueState?.usdRate ?? 0.0
|
||||
// )))
|
||||
// ],
|
||||
// displaySeparators: false
|
||||
// )),
|
||||
// environment: {},
|
||||
// containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height)
|
||||
// )
|
||||
// let proceedsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - proceedsSize.width) / 2.0), y: contentHeight), size: proceedsSize)
|
||||
// if let proceedsView = self.proceedsView.view {
|
||||
// if proceedsView.superview == nil {
|
||||
// self.scrollView.addSubview(proceedsView)
|
||||
// }
|
||||
// transition.setFrame(view: proceedsView, frame: proceedsFrame)
|
||||
// }
|
||||
// contentHeight += proceedsSize.height
|
||||
// contentHeight += 31.0
|
||||
// }
|
||||
|
||||
let termsFont = Font.regular(13.0)
|
||||
let termsTextColor = environment.theme.list.freeTextColor
|
||||
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
})
|
||||
|
||||
let balanceInfoRawString = "Collect your TON using Fragment. [Learn More >]()"
|
||||
let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(balanceInfoRawString, attributes: termsMarkdownAttributes, textAlignment: .natural))
|
||||
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme {
|
||||
self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme)
|
||||
}
|
||||
if let range = balanceInfoString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 {
|
||||
balanceInfoString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceInfoString.string))
|
||||
}
|
||||
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
let balanceSize = self.balanceView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: nil,
|
||||
footer: nil,
|
||||
footer: component.starsContext.ton && !"".isEmpty ? AnyComponent(MultilineTextComponent(
|
||||
text: .plain(balanceInfoString),
|
||||
maximumNumberOfLines: 0,
|
||||
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
|
||||
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { [weak self] attributes, _ in
|
||||
if let controller = self?.controller?() as? StarsTransactionsScreen, let navigationController = controller.navigationController as? NavigationController {
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: environment.strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
|
||||
}
|
||||
}
|
||||
)) : nil,
|
||||
items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
StarsBalanceComponent(
|
||||
theme: environment.theme,
|
||||
|
|
@ -683,7 +765,7 @@ final class StarsTransactionsScreenComponent: Component {
|
|||
count: self.starsState?.balance ?? StarsAmount.zero,
|
||||
currency: component.starsContext.ton ? .ton : .stars,
|
||||
rate: nil,
|
||||
actionTitle: (withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy),
|
||||
actionTitle: component.starsContext.ton ? "Withdraw via Fragment" : (withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy),
|
||||
actionAvailable: (!component.starsContext.ton && !premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled),
|
||||
actionIsEnabled: true,
|
||||
actionIcon: component.starsContext.ton ? nil : PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme),
|
||||
|
|
@ -1270,51 +1352,53 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
|
|||
return
|
||||
}
|
||||
|
||||
let controller = self.context.sharedContext.makeStarsStatisticsScreen(context: context, peerId: context.account.peerId, revenueContext: self.starsRevenueStatsContext)
|
||||
self.push(controller)
|
||||
|
||||
// let _ = (context.engine.peers.checkStarsRevenueWithdrawalAvailability()
|
||||
// |> deliverOnMainQueue).start(error: { [weak self] error in
|
||||
// guard let self else {
|
||||
// return
|
||||
// }
|
||||
// switch error {
|
||||
// case .serverProvided:
|
||||
// return
|
||||
// case .requestPassword:
|
||||
// let _ = (self.starsRevenueStatsContext.state
|
||||
// |> take(1)
|
||||
// |> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
// guard let self else {
|
||||
// return
|
||||
// }
|
||||
// let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, completion: { [weak self] amount in
|
||||
// guard let self else {
|
||||
// return
|
||||
// }
|
||||
// let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: amount, present: { [weak self] c, a in
|
||||
// self?.present(c, in: .window(.root))
|
||||
// }, completion: { [weak self] 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: {})
|
||||
//
|
||||
// Queue.mainQueue().after(2.0) {
|
||||
// self?.starsRevenueStatsContext.reload()
|
||||
// }
|
||||
// })
|
||||
// self.present(controller, in: .window(.root))
|
||||
// })
|
||||
// self.push(controller)
|
||||
// })
|
||||
// default:
|
||||
// let controller = starsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: 0, initialError: error, present: { [weak self] c, a in
|
||||
// self?.present(c, in: .window(.root))
|
||||
// }, completion: { _ in
|
||||
//
|
||||
// })
|
||||
// self.present(controller, in: .window(.root))
|
||||
// }
|
||||
// })
|
||||
if self.starsContext.ton {
|
||||
let _ = (context.engine.peers.checkStarsRevenueWithdrawalAvailability()
|
||||
|> deliverOnMainQueue).start(error: { [weak self] error in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch error {
|
||||
case .serverProvided:
|
||||
return
|
||||
case .requestPassword:
|
||||
let _ = (self.starsRevenueStatsContext.state
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self, let stats = state.stats else {
|
||||
return
|
||||
}
|
||||
let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, stats: stats, completion: { [weak self] amount in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: amount, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root))
|
||||
}, completion: { [weak self] 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: {})
|
||||
|
||||
Queue.mainQueue().after(2.0) {
|
||||
self?.starsRevenueStatsContext.reload()
|
||||
}
|
||||
})
|
||||
self.present(controller, in: .window(.root))
|
||||
})
|
||||
self.push(controller)
|
||||
})
|
||||
default:
|
||||
let controller = starsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: 0, initialError: error, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root))
|
||||
}, completion: { _ in
|
||||
|
||||
})
|
||||
self.present(controller, in: .window(.root))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let controller = self.context.sharedContext.makeStarsStatisticsScreen(context: context, peerId: context.account.peerId, revenueContext: self.starsRevenueStatsContext)
|
||||
self.push(controller)
|
||||
}
|
||||
}
|
||||
|
||||
showTimeoutTooltipImpl = { [weak self] cooldownUntilTimestamp in
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/AddCollection.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/AddCollection.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "addfolder_24.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.55664 4.335C7.84623 4.335 8.08437 4.33202 8.31641 4.37992C8.5843 4.43527 8.83979 4.54115 9.06836 4.69144C9.26634 4.82169 9.43285 4.99246 9.6377 5.1973L10.2676 5.82719C10.5486 6.10823 10.6161 6.1699 10.6846 6.21195C10.7593 6.25772 10.8416 6.29111 10.9268 6.31156C11.005 6.3303 11.0961 6.335 11.4941 6.335H15C15.887 6.335 16.4609 6.32949 16.9482 6.46C18.213 6.7989 19.2012 7.78704 19.54 9.05179C19.6379 9.41722 19.6595 9.83137 19.6641 10.3897L19.665 11V12.5C19.665 12.8673 19.3673 13.1651 19 13.1651C18.6327 13.1651 18.335 12.8673 18.335 12.5V11.6651H3.66504V14.2002C3.66504 15.0512 3.66596 15.6494 3.7041 16.1162C3.74161 16.5754 3.81216 16.8491 3.91992 17.0606C4.14371 17.4995 4.50053 17.8563 4.93945 18.0801C5.15095 18.1879 5.42468 18.2584 5.88379 18.2959C6.35064 18.3341 6.94888 18.335 7.79981 18.335H12.5C12.8672 18.335 13.165 18.6328 13.165 19C13.165 19.3215 12.9369 19.5895 12.6338 19.6514L12.5 19.6651H7.79981C6.97081 19.6651 6.30896 19.6657 5.77539 19.6221C5.23404 19.5779 4.76616 19.4849 4.33594 19.2657C3.64642 18.9143 3.08574 18.3536 2.73438 17.6641C2.51516 17.2339 2.42216 16.766 2.37793 16.2246C2.33434 15.6911 2.33496 15.0292 2.33496 14.2002V9.55668C2.33496 8.84099 2.33448 8.26933 2.36719 7.80668C2.40037 7.33776 2.46998 6.9298 2.63477 6.54789C3.00512 5.68981 3.68977 5.00516 4.54785 4.6348C4.92976 4.47002 5.33774 4.40041 5.80664 4.36722C6.26929 4.33451 6.84098 4.335 7.55664 4.335ZM7.55664 5.66508C6.82202 5.66508 6.30524 5.66574 5.90039 5.69437C5.50202 5.72256 5.26219 5.77583 5.0752 5.85648C4.52851 6.09244 4.0924 6.52855 3.85645 7.07523C3.77579 7.26223 3.72253 7.50204 3.69434 7.90043C3.6657 8.30528 3.66504 8.82204 3.66504 9.55668V10.335H18.332C18.3266 9.84038 18.3097 9.59655 18.2559 9.39554C18.0399 8.58979 17.4102 7.9601 16.6045 7.74418C16.3289 7.67033 15.9728 7.66508 15 7.66508H11.4941C11.1591 7.66508 10.8831 7.6695 10.6162 7.60551C10.3953 7.55247 10.1839 7.46444 9.99023 7.34574C9.75626 7.20235 9.56404 7.00451 9.32715 6.76762L8.69727 6.13773C8.4545 5.89496 8.39582 5.84096 8.33789 5.80277C8.24983 5.74486 8.15106 5.70402 8.04785 5.68265C7.97977 5.66859 7.90072 5.66508 7.55664 5.66508Z" fill="black"/>
|
||||
<path d="M19 14.335C19.3673 14.335 19.665 14.6327 19.665 15V18.335H23L23.1338 18.3486C23.4369 18.4106 23.665 18.6786 23.665 19C23.665 19.3214 23.4369 19.5894 23.1338 19.6514L23 19.665H19.665V23C19.665 23.3672 19.3672 23.665 19 23.665C18.6327 23.665 18.335 23.3673 18.335 23V19.665H15C14.6327 19.665 14.335 19.3673 14.335 19C14.335 18.6327 14.6327 18.335 15 18.335H18.335V15C18.335 14.6327 18.6327 14.335 19 14.335Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
12
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/AddGift.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/AddGift.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "addgift_24.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
4
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/AddGift.imageset/addgift_24.svg
vendored
Normal file
4
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/AddGift.imageset/addgift_24.svg
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 0.834961C15.4718 0.834983 16.665 2.02817 16.665 3.5C16.665 3.98684 16.5327 4.44219 16.3047 4.83496H16.875C17.3894 4.83496 17.8128 4.83493 18.1572 4.86133C18.5086 4.88828 18.8326 4.94562 19.1396 5.09082C19.6969 5.35442 20.1456 5.80313 20.4092 6.36035C20.5544 6.66735 20.6117 6.99135 20.6387 7.34277C20.6651 7.68718 20.665 8.11058 20.665 8.625C20.665 8.92954 20.6652 9.19587 20.6484 9.41504C20.6311 9.64112 20.5931 9.87197 20.4863 10.0977C20.312 10.4663 20.0208 10.7655 19.6602 10.9531C19.6612 10.9687 19.665 10.9842 19.665 11V12C19.665 12.3672 19.3672 12.665 19 12.665C18.6327 12.665 18.335 12.3673 18.335 12V11.1641C18.2672 11.1643 18.1972 11.165 18.125 11.165H12.665V19L12.6543 19.1182C12.6538 19.1209 12.6519 19.1233 12.6514 19.126C12.6495 19.1359 12.6469 19.1455 12.6445 19.1553C12.6368 19.1876 12.6266 19.2187 12.6143 19.249C12.6101 19.2593 12.6053 19.2692 12.6006 19.2793C12.589 19.3042 12.577 19.3285 12.5625 19.3516C12.5527 19.3673 12.5413 19.3817 12.5303 19.3965C12.5165 19.4149 12.5019 19.4324 12.4863 19.4492C12.4758 19.4606 12.4654 19.4718 12.4541 19.4824C12.4374 19.4982 12.4197 19.5124 12.4014 19.5264C12.386 19.5381 12.3709 19.5501 12.3545 19.5605C12.3329 19.5742 12.3102 19.5855 12.2871 19.5967C12.2757 19.6022 12.2647 19.6084 12.2529 19.6133C12.2147 19.6291 12.1752 19.6429 12.1338 19.6514L12 19.665H8.2002C7.65114 19.665 7.19889 19.6657 6.83203 19.6357C6.45755 19.6051 6.11321 19.5396 5.79004 19.375C5.2886 19.1195 4.8805 18.7114 4.625 18.21C4.46036 17.8868 4.39487 17.5425 4.36426 17.168C4.33428 16.8011 4.33496 16.3489 4.33496 15.7998V11C4.33496 10.9842 4.33778 10.9686 4.33887 10.9531C3.97859 10.7655 3.68793 10.466 3.51367 10.0977C3.40693 9.87197 3.36892 9.64112 3.35156 9.41504C3.33476 9.19587 3.33496 8.92954 3.33496 8.625C3.33496 8.11058 3.33493 7.68718 3.36133 7.34277C3.38827 6.99134 3.44562 6.66736 3.59082 6.36035C3.85442 5.80311 4.30311 5.35442 4.86035 5.09082C5.16736 4.94562 5.49134 4.88827 5.84277 4.86133C6.18718 4.83493 6.61058 4.83496 7.125 4.83496H7.69531C7.46729 4.44218 7.33497 3.98686 7.33496 3.5C7.33496 2.02816 8.52816 0.834961 10 0.834961C10.7976 0.834972 11.5116 1.18691 12 1.74219C12.4884 1.18693 13.2024 0.834961 14 0.834961ZM5.66504 15.7998C5.66504 16.3708 5.66591 16.7594 5.69043 17.0596C5.71432 17.3518 5.75729 17.5019 5.81055 17.6064C5.9385 17.8574 6.14262 18.0615 6.39355 18.1895C6.49807 18.2427 6.6482 18.2857 6.94043 18.3096C7.24057 18.3341 7.6292 18.335 8.2002 18.335H11.335V11.165H5.875C5.80284 11.165 5.73279 11.1643 5.66504 11.1641V15.7998ZM12.665 9.83496H18.125C18.4498 9.83496 18.656 9.83423 18.8125 9.82227C18.9615 9.81084 19.0104 9.79164 19.0283 9.7832C19.1402 9.73028 19.2303 9.6402 19.2832 9.52832C19.2916 9.51042 19.3108 9.46147 19.3223 9.3125C19.3342 9.15601 19.335 8.94976 19.335 8.625C19.335 8.09005 19.3341 7.72612 19.3125 7.44434C19.2915 7.1702 19.2537 7.0286 19.207 6.92969C19.075 6.6505 18.8495 6.42503 18.5703 6.29297C18.4714 6.24628 18.3298 6.20853 18.0557 6.1875C17.7739 6.1659 17.41 6.16504 16.875 6.16504H12.665V9.83496ZM7.125 6.16504C6.59005 6.16504 6.22612 6.1659 5.94434 6.1875C5.67018 6.20853 5.5286 6.24628 5.42969 6.29297C5.15048 6.42502 4.92502 6.65048 4.79297 6.92969C4.74628 7.0286 4.70853 7.17018 4.6875 7.44434C4.6659 7.72612 4.66504 8.09005 4.66504 8.625C4.66504 8.94976 4.66577 9.15601 4.67773 9.3125C4.68916 9.46146 4.70836 9.51042 4.7168 9.52832C4.76972 9.64019 4.8598 9.73029 4.97168 9.7832C4.98962 9.79165 5.03863 9.81085 5.1875 9.82227C5.34399 9.83423 5.55025 9.83496 5.875 9.83496H11.335V6.16504H7.125ZM10 2.16504C9.2627 2.16504 8.66504 2.7627 8.66504 3.5C8.66506 4.23728 9.26271 4.83496 10 4.83496H11.335V3.5C11.335 2.76271 10.7373 2.16506 10 2.16504ZM14 2.16504C13.2627 2.16504 12.665 2.7627 12.665 3.5V4.83496H14C14.7373 4.83494 15.3349 4.23727 15.335 3.5C15.335 2.76271 14.7373 2.16506 14 2.16504Z" fill="black"/>
|
||||
<path d="M19 14.335C19.3673 14.335 19.665 14.6327 19.665 15V18.335H23L23.1338 18.3486C23.4369 18.4106 23.665 18.6786 23.665 19C23.665 19.3214 23.4369 19.5894 23.1338 19.6514L23 19.665H19.665V23C19.665 23.3672 19.3672 23.665 19 23.665C18.6327 23.665 18.335 23.3673 18.335 23V19.665H15C14.6327 19.665 14.335 19.3673 14.335 19C14.335 18.6327 14.6327 18.335 15 18.335H18.335V15C18.335 14.6327 18.6327 14.335 19 14.335Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
12
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/AddToCollection.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/AddToCollection.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "fromfolder_24.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.66504 14.2002C4.66504 15.0512 4.66596 15.6494 4.7041 16.1162C4.74161 16.5754 4.81216 16.8491 4.91992 17.0606C5.14371 17.4995 5.50053 17.8563 5.93945 18.0801C6.15095 18.1879 6.42468 18.2584 6.88379 18.2959C7.35064 18.3341 7.94888 18.335 8.79981 18.335H15.2002C16.0511 18.335 16.6494 18.3341 17.1162 18.2959C17.5753 18.2584 17.8491 18.1879 18.0605 18.0801C18.4995 17.8563 18.8563 17.4995 19.0801 17.0606C19.1878 16.8491 19.2584 16.5754 19.2959 16.1162C19.334 15.6494 19.335 15.0512 19.335 14.2002V11C19.335 10.0273 19.3297 9.67116 19.2559 9.39554C19.0399 8.58979 18.4102 7.9601 17.6045 7.74418C17.3289 7.67033 16.9728 7.66508 16 7.66508H12.4941C12.1591 7.66508 11.8831 7.6695 11.6162 7.60551C11.3953 7.55247 11.1839 7.46444 10.9902 7.34574C10.7563 7.20235 10.564 7.00451 10.3271 6.76762L9.69727 6.13773C9.4545 5.89496 9.39582 5.84096 9.33789 5.80277C9.24983 5.74486 9.15106 5.70402 9.04785 5.68265C8.97977 5.66859 8.90072 5.66508 8.55664 5.66508C7.82202 5.66508 7.30524 5.66574 6.90039 5.69437C6.50202 5.72256 6.26219 5.77583 6.0752 5.85648C5.52851 6.09244 5.0924 6.52855 4.85645 7.07523C4.77579 7.26223 4.72253 7.50204 4.69434 7.90043C4.6657 8.30528 4.66504 8.82204 4.66504 9.55668V14.2002ZM12 9.335C12.3672 9.33503 12.665 9.63279 12.665 10V14.3936L14.5293 12.5293C14.789 12.2697 15.211 12.2696 15.4707 12.5293C15.7304 12.789 15.7304 13.211 15.4707 13.4707L12.4707 16.4707C12.211 16.7304 11.789 16.7304 11.5293 16.4707L8.5293 13.4707L8.44434 13.3662C8.27383 13.1081 8.30203 12.7566 8.5293 12.5293C8.75657 12.3021 9.10809 12.2739 9.36621 12.4444L9.4707 12.5293L11.335 14.3936V10C11.335 9.63277 11.6327 9.335 12 9.335ZM20.665 14.2002C20.665 15.0292 20.6657 15.6911 20.6221 16.2246C20.5778 16.766 20.4848 17.2339 20.2656 17.6641C19.9143 18.3536 19.3536 18.9143 18.6641 19.2657C18.2338 19.4849 17.766 19.5779 17.2246 19.6221C16.691 19.6657 16.0292 19.6651 15.2002 19.6651H8.79981C7.97081 19.6651 7.30896 19.6657 6.77539 19.6221C6.23404 19.5779 5.76616 19.4849 5.33594 19.2657C4.64642 18.9143 4.08574 18.3536 3.73438 17.6641C3.51516 17.2339 3.42216 16.766 3.37793 16.2246C3.33434 15.6911 3.33496 15.0292 3.33496 14.2002V9.55668C3.33496 8.84099 3.33448 8.26933 3.36719 7.80668C3.40037 7.33776 3.46998 6.9298 3.63477 6.54789C4.00512 5.68981 4.68977 5.00516 5.54785 4.6348C5.92976 4.47002 6.33774 4.40041 6.80664 4.36722C7.26929 4.33451 7.84098 4.335 8.55664 4.335C8.84623 4.335 9.08437 4.33202 9.31641 4.37992C9.5843 4.43527 9.83979 4.54115 10.0684 4.69144C10.2663 4.82169 10.4329 4.99246 10.6377 5.1973L11.2676 5.82719C11.5486 6.10823 11.6161 6.1699 11.6846 6.21195C11.7593 6.25772 11.8416 6.29111 11.9268 6.31156C12.005 6.3303 12.0961 6.335 12.4941 6.335H16C16.887 6.335 17.4609 6.32949 17.9482 6.46C19.213 6.7989 20.2012 7.78704 20.54 9.05179C20.6706 9.53911 20.665 10.1131 20.665 11V14.2002Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
12
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/Collection.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/Collection.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "folder_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/Collection.imageset/folder_24.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/Gifts/Collection.imageset/folder_24.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "tofolder_24.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.66504 14.2002C4.66504 15.0512 4.66596 15.6494 4.7041 16.1162C4.74161 16.5754 4.81216 16.8491 4.91992 17.0606C5.14371 17.4995 5.50053 17.8563 5.93945 18.0801C6.15095 18.1879 6.42468 18.2584 6.88379 18.2959C7.35064 18.3341 7.94888 18.335 8.79981 18.335H15.2002C16.0511 18.335 16.6494 18.3341 17.1162 18.2959C17.5753 18.2584 17.8491 18.1879 18.0605 18.0801C18.4995 17.8563 18.8563 17.4995 19.0801 17.0606C19.1878 16.8491 19.2584 16.5754 19.2959 16.1162C19.334 15.6494 19.335 15.0512 19.335 14.2002V11C19.335 10.0273 19.3297 9.67116 19.2559 9.39554C19.0399 8.58979 18.4102 7.9601 17.6045 7.74418C17.3289 7.67033 16.9728 7.66508 16 7.66508H12.4941C12.1591 7.66508 11.8831 7.6695 11.6162 7.60551C11.3953 7.55247 11.1839 7.46444 10.9902 7.34574C10.7563 7.20235 10.564 7.00451 10.3271 6.76762L9.69727 6.13773C9.4545 5.89496 9.39582 5.84096 9.33789 5.80277C9.24983 5.74486 9.15106 5.70402 9.04785 5.68265C8.97977 5.66859 8.90072 5.66508 8.55664 5.66508C7.82202 5.66508 7.30524 5.66574 6.90039 5.69437C6.50202 5.72256 6.26219 5.77583 6.0752 5.85648C5.52851 6.09244 5.0924 6.52855 4.85645 7.07523C4.77579 7.26223 4.72253 7.50204 4.69434 7.90043C4.6657 8.30528 4.66504 8.82204 4.66504 9.55668V14.2002ZM11.6338 9.44437C11.8919 9.27389 12.2434 9.30207 12.4707 9.52933L15.4707 12.5293L15.5557 12.6338C15.7262 12.8919 15.6979 13.2435 15.4707 13.4707C15.2434 13.698 14.8919 13.7262 14.6338 13.5557L14.5293 13.4707L12.665 11.6065V16C12.665 16.3673 12.3673 16.6651 12 16.6651C11.6327 16.6651 11.335 16.3673 11.335 16V11.6065L9.4707 13.4707C9.21101 13.7304 8.789 13.7304 8.5293 13.4707C8.2696 13.211 8.2696 12.789 8.5293 12.5293L11.5293 9.52933L11.6338 9.44437ZM20.665 14.2002C20.665 15.0292 20.6657 15.6911 20.6221 16.2246C20.5778 16.766 20.4848 17.2339 20.2656 17.6641C19.9143 18.3536 19.3536 18.9143 18.6641 19.2657C18.2338 19.4849 17.766 19.5779 17.2246 19.6221C16.691 19.6657 16.0292 19.6651 15.2002 19.6651H8.79981C7.97081 19.6651 7.30896 19.6657 6.77539 19.6221C6.23404 19.5779 5.76616 19.4849 5.33594 19.2657C4.64642 18.9143 4.08574 18.3536 3.73438 17.6641C3.51516 17.2339 3.42216 16.766 3.37793 16.2246C3.33434 15.6911 3.33496 15.0292 3.33496 14.2002V9.55668C3.33496 8.84099 3.33448 8.26933 3.36719 7.80668C3.40037 7.33776 3.46998 6.9298 3.63477 6.54789C4.00512 5.68981 4.68977 5.00516 5.54785 4.6348C5.92976 4.47002 6.33774 4.40041 6.80664 4.36722C7.26929 4.33451 7.84098 4.335 8.55664 4.335C8.84623 4.335 9.08437 4.33202 9.31641 4.37992C9.5843 4.43527 9.83979 4.54115 10.0684 4.69144C10.2663 4.82169 10.4329 4.99246 10.6377 5.1973L11.2676 5.82719C11.5486 6.10823 11.6161 6.1699 11.6846 6.21195C11.7593 6.25772 11.8416 6.29111 11.9268 6.31156C12.005 6.3303 12.0961 6.335 12.4941 6.335H16C16.887 6.335 17.4609 6.32949 17.9482 6.46C19.213 6.7989 20.2012 7.78704 20.54 9.05179C20.6706 9.53911 20.665 10.1131 20.665 11V14.2002Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
12
submodules/TelegramUI/Images.xcassets/Settings/FaceVerification.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Settings/FaceVerification.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "faceid.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Settings/FaceVerification.imageset/faceid.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Settings/FaceVerification.imageset/faceid.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -122,9 +122,10 @@ import PeerNameColorScreen
|
|||
import ChatEmptyNode
|
||||
import ChatMediaInputStickerGridItem
|
||||
import AdsInfoScreen
|
||||
import FaceScanScreen
|
||||
|
||||
extension ChatControllerImpl {
|
||||
func openPeer(peer: EnginePeer?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: MessageReference?, fromReactionMessageId: MessageId? = nil, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil) {
|
||||
func openPeer(peer: EnginePeer?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: MessageReference?, fromReactionMessageId: MessageId? = nil, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil, skipAgeVerification: Bool = false) {
|
||||
let _ = self.presentVoiceMessageDiscardAlert(action: {
|
||||
if case let .peer(currentPeerId) = self.chatLocation, peer?.id == currentPeerId {
|
||||
switch navigation {
|
||||
|
|
@ -223,11 +224,23 @@ extension ChatControllerImpl {
|
|||
}
|
||||
})
|
||||
} else {
|
||||
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
|
||||
self.effectiveNavigationController?.pushViewController(ChatListControllerImpl(context: self.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, enableDebugActions: false))
|
||||
} else {
|
||||
self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(id: peer.id), subject: subject))
|
||||
}
|
||||
let _ = (requireAgeVerification(context: self.context, peer: peer)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] require in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if require && !skipAgeVerification {
|
||||
presentAgeVerification(context: self.context, parentController: self, completion: {
|
||||
self.openPeer(peer: peer, navigation: navigation, fromMessage: fromMessage, fromReactionMessageId: fromReactionMessageId, expandAvatar: expandAvatar, peerTypes: peerTypes)
|
||||
})
|
||||
} else {
|
||||
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
|
||||
self.effectiveNavigationController?.pushViewController(ChatListControllerImpl(context: self.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, enableDebugActions: false))
|
||||
} else {
|
||||
self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(id: peer.id), subject: subject))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
case let .withBotStartPayload(botStart):
|
||||
self.effectiveNavigationController?.pushViewController(ChatControllerImpl(context: self.context, chatLocation: .peer(id: peer.id), botStart: botStart))
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ extension ChatControllerImpl {
|
|||
}
|
||||
|
||||
if "".isEmpty {
|
||||
//TODO:localize
|
||||
//TODO:release
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ReplyToItem, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ private final class ChatAgeRestrictionAlertContentNode: AlertContentNode {
|
|||
}
|
||||
}
|
||||
|
||||
public func chatAgeRestrictionAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, completion: @escaping (Bool) -> Void) -> AlertController {
|
||||
public func chatAgeRestrictionAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, parentController: ViewController, completion: @escaping (Bool) -> Void) -> AlertController {
|
||||
let theme = defaultDarkColorPresentationTheme
|
||||
let presentationData: PresentationData
|
||||
if let updatedPresentationData {
|
||||
|
|
@ -268,7 +268,7 @@ public func chatAgeRestrictionAlertController(context: AccountContext, updatedPr
|
|||
presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
}
|
||||
let strings = presentationData.strings
|
||||
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var getContentNodeImpl: (() -> ChatAgeRestrictionAlertContentNode?)?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: strings.SensitiveContent_ViewAnyway, action: {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ import PromptUI
|
|||
import SuggestedPostApproveAlert
|
||||
import AVFoundation
|
||||
import BalanceNeededScreen
|
||||
import FaceScanScreen
|
||||
|
||||
public final class ChatControllerOverlayPresentationData {
|
||||
public let expandData: (ASDisplayNode?, () -> Void)
|
||||
|
|
@ -4770,10 +4771,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = chatAgeRestrictionAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, completion: { _ in
|
||||
reveal()
|
||||
})
|
||||
self.present(controller, in: .window(.root))
|
||||
if requireAgeVerification(context: context) {
|
||||
presentAgeVerification(context: context, parentController: self, completion: {
|
||||
let _ = updateRemoteContentSettingsConfiguration(postbox: context.account.postbox, network: context.account.network, sensitiveContentEnabled: true).start()
|
||||
reveal()
|
||||
})
|
||||
} else {
|
||||
let controller = chatAgeRestrictionAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, parentController: self, completion: { alwaysShow in
|
||||
if alwaysShow {
|
||||
let _ = updateRemoteContentSettingsConfiguration(postbox: context.account.postbox, network: context.account.network, sensitiveContentEnabled: true).start()
|
||||
}
|
||||
reveal()
|
||||
})
|
||||
self.present(controller, in: .window(.root))
|
||||
}
|
||||
}, playMessageEffect: { [weak self] message in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -20,12 +20,18 @@ import ChatControllerInteraction
|
|||
import SavedMessagesScreen
|
||||
import WallpaperGalleryScreen
|
||||
import ChatMessageNotificationItem
|
||||
import FaceScanScreen
|
||||
|
||||
public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) {
|
||||
if case let .peer(peer) = params.chatLocation {
|
||||
let _ = params.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer).startStandalone()
|
||||
}
|
||||
|
||||
var requiresAgeVerification: Signal<Bool, NoError> = .single(false)
|
||||
if !params.skipAgeVerification, case let .peer(peer) = params.chatLocation {
|
||||
requiresAgeVerification = requireAgeVerification(context: params.context, peer: peer)
|
||||
}
|
||||
|
||||
var viewForumAsMessages: Signal<Bool, NoError> = .single(false)
|
||||
if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isMonoforum) {
|
||||
if let linkedMonoforumId = channel.linkedMonoforumId {
|
||||
|
|
@ -67,9 +73,18 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
|||
}
|
||||
}
|
||||
|
||||
let _ = (viewForumAsMessages
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { viewForumAsMessages in
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
viewForumAsMessages |> take(1),
|
||||
requiresAgeVerification
|
||||
).start(next: { viewForumAsMessages, requiresAgeVerification in
|
||||
if requiresAgeVerification, let parentController = params.navigationController.viewControllers.last as? ViewController {
|
||||
presentAgeVerification(context: params.context, parentController: parentController, completion: {
|
||||
navigateToChatControllerImpl(params.withSkipAgeVerification(true))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isForum), !viewForumAsMessages {
|
||||
for controller in params.navigationController.viewControllers.reversed() {
|
||||
var chatListController: ChatListControllerImpl?
|
||||
|
|
@ -118,9 +133,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
|||
controller.activateSearch(query: activateMessageSearch.1)
|
||||
}
|
||||
|
||||
if let chatListCompletion {
|
||||
chatListCompletion(controller)
|
||||
}
|
||||
chatListCompletion(controller)
|
||||
})
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import PeerInfoScreen
|
|||
import PeerInfoStoryGridScreen
|
||||
import ShareWithPeersScreen
|
||||
import ChatEmptyNode
|
||||
//import FaceScanScreen
|
||||
import UndoUI
|
||||
|
||||
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
||||
private var presentationData: PresentationData
|
||||
|
|
@ -235,6 +237,22 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||
self.accountSettingsController = accountSettingsController
|
||||
self.rootTabController = tabBarController
|
||||
self.pushViewController(tabBarController, animated: false)
|
||||
|
||||
// Queue.mainQueue().after(1.0) {
|
||||
// let context = self.context
|
||||
// let infoScreen = AgeVerificationScreen(context: context, completion: { [weak chatListController] proceed in
|
||||
// if proceed {
|
||||
// let scanScreen = FaceScanScreen(context: context, completion: { success in
|
||||
// let controller = UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: "Age check passed!", text: "You can now view this content.", cancel: nil, destructive: false), elevatedLayout: true, action: { _ in return true })
|
||||
// Queue.mainQueue().after(0.1) {
|
||||
// chatListController?.present(controller, in: .window(.root))
|
||||
// }
|
||||
// })
|
||||
// chatListController?.push(scanScreen)
|
||||
// }
|
||||
// })
|
||||
// chatListController.push(infoScreen)
|
||||
// }
|
||||
}
|
||||
|
||||
public func updateRootControllers(showCallsTab: Bool) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 {
|
|||
case chatListFilterSettings = 18
|
||||
case widgetSettings = 19
|
||||
case mediaAutoSaveSettings = 20
|
||||
case ageVerificationState = 21
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificPreferencesKeys {
|
||||
|
|
@ -16,6 +17,7 @@ public struct ApplicationSpecificPreferencesKeys {
|
|||
public static let chatListFilterSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatListFilterSettings.rawValue)
|
||||
public static let widgetSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.widgetSettings.rawValue)
|
||||
public static let mediaAutoSaveSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.mediaAutoSaveSettings.rawValue)
|
||||
public static let ageVerificationState = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.ageVerificationState.rawValue)
|
||||
}
|
||||
|
||||
private enum ApplicationSpecificSharedDataKeyValues: Int32 {
|
||||
|
|
|
|||
|
|
@ -48,11 +48,10 @@ private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(in
|
|||
" style.appendChild(document.createTextNode(css)); head.appendChild(style);"
|
||||
|
||||
private let videoSource = """
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
function tgBrowserDisableWebkitEnterFullscreen(videoElement) {
|
||||
if (videoElement && videoElement.webkitEnterFullscreen) {
|
||||
Object.defineProperty(videoElement, 'webkitEnterFullscreen', {
|
||||
value: undefined
|
||||
});
|
||||
videoElement.setAttribute('playsinline', '');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +86,7 @@ _tgbrowser_observer.observe(document.body, {
|
|||
function tgBrowserDisconnectObserver() {
|
||||
_tgbrowser_observer.disconnect();
|
||||
}
|
||||
});
|
||||
"""
|
||||
|
||||
final class WebAppWebView: WKWebView {
|
||||
|
|
@ -151,9 +151,9 @@ final class WebAppWebView: WKWebView {
|
|||
configuration.allowsInlineMediaPlayback = true
|
||||
configuration.allowsPictureInPictureMediaPlayback = false
|
||||
if #available(iOS 10.0, *) {
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = .audio
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||
} else {
|
||||
configuration.mediaPlaybackRequiresUserAction = true
|
||||
configuration.mediaPlaybackRequiresUserAction = false
|
||||
}
|
||||
|
||||
super.init(frame: CGRect(), configuration: configuration)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"app": "11.13.2",
|
||||
"app": "11.14",
|
||||
"xcode": "16.2",
|
||||
"bazel": "8.3.1:0cac3a67dc5429c68272dc6944104952e9e4cf84b29d126a5ff3fbaa59045217",
|
||||
"macos": "15"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue