diff --git a/.gitmodules b/.gitmodules index 5fd5f2e00e..f931b28d97 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ + [submodule "submodules/rlottie/rlottie"] path = submodules/rlottie/rlottie - url = https://github.com/peter-iakovlev/rlottie.git + url = https://github.com/laktyushin/rlottie.git diff --git a/BUCK b/BUCK index 2f5be86367..bc6870f526 100644 --- a/BUCK +++ b/BUCK @@ -399,8 +399,9 @@ apple_binary( "//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider", ], frameworks = [ - "$SDKROOT/System/Library/Frameworks/UIKit.framework", "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/Intents.framework", + "$SDKROOT/System/Library/Frameworks/Contacts.framework", ], ) diff --git a/SiriIntents/IntentHandler.swift b/SiriIntents/IntentHandler.swift index fb0e446b30..071db1ed78 100644 --- a/SiriIntents/IntentHandler.swift +++ b/SiriIntents/IntentHandler.swift @@ -343,10 +343,8 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag func handle(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { self.actionDisposable.set((self.accountPromise.get() + |> castError(IntentHandlingError.self) |> take(1) - |> mapError { _ -> IntentHandlingError in - return .generic - } |> mapToSignal { account -> Signal in guard let account = account else { return .fail(.generic) @@ -456,10 +454,8 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag func handle(intent: INSetMessageAttributeIntent, completion: @escaping (INSetMessageAttributeIntentResponse) -> Void) { self.actionDisposable.set((self.accountPromise.get() + |> castError(IntentHandlingError.self) |> take(1) - |> mapError { _ -> IntentHandlingError in - return .generic - } |> mapToSignal { account -> Signal in guard let account = account else { return .fail(.generic) @@ -532,10 +528,8 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag func handle(intent: INStartAudioCallIntent, completion: @escaping (INStartAudioCallIntentResponse) -> Void) { self.actionDisposable.set((self.accountPromise.get() + |> castError(IntentHandlingError.self) |> take(1) - |> mapError { _ -> IntentHandlingError in - return .generic - } |> mapToSignal { account -> Signal in guard let contact = intent.contacts?.first, let customIdentifier = contact.customIdentifier, customIdentifier.hasPrefix("tg") else { return .fail(.generic) diff --git a/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/Contents.json b/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/Contents.json new file mode 100644 index 0000000000..868e9f5048 --- /dev/null +++ b/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_camera.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/ic_camera.pdf b/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/ic_camera.pdf new file mode 100644 index 0000000000..a5e2d3c939 Binary files /dev/null and b/Telegram-iOS/Icons.xcassets/Shortcuts/Camera.imageset/ic_camera.pdf differ diff --git a/Telegram-iOS/en.lproj/Localizable.strings b/Telegram-iOS/en.lproj/Localizable.strings index 85dd48271e..d963ab180f 100644 --- a/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram-iOS/en.lproj/Localizable.strings @@ -4638,12 +4638,12 @@ Any member of this group will be able to see messages in the channel."; "Conversation.SendMessage.SetReminder" = "Set a Reminder"; -"Conversation.SelectedMessages_1" = "%@ Message Selected"; -"Conversation.SelectedMessages_2" = "%@ Messages Selected"; -"Conversation.SelectedMessages_3_10" = "%@ Messages Selected"; -"Conversation.SelectedMessages_any" = "%@ Messages Selected"; -"Conversation.SelectedMessages_many" = "%@ Messages Selected"; -"Conversation.SelectedMessages_0" = "%@ Messages Selected"; +"Conversation.SelectedMessages_1" = "%@ Selected"; +"Conversation.SelectedMessages_2" = "%@ Selected"; +"Conversation.SelectedMessages_3_10" = "%@ Selected"; +"Conversation.SelectedMessages_any" = "%@ Selected"; +"Conversation.SelectedMessages_many" = "%@ Selected"; +"Conversation.SelectedMessages_0" = "%@ Selected"; "AccentColor.Title" = "Accent Color"; @@ -4872,11 +4872,11 @@ Any member of this group will be able to see messages in the channel."; "Wallet.TransactionInfo.SenderHeader" = "SENDER"; "Wallet.TransactionInfo.CopyAddress" = "Copy Wallet Address"; "Wallet.TransactionInfo.AddressCopied" = "Address copied to clipboard."; -"Wallet.TransactionInfo.SendGrams" = "Send Grams"; +"Wallet.TransactionInfo.SendGrams" = "Send Grams to This Address"; "Wallet.TransactionInfo.CommentHeader" = "COMMENT"; "Wallet.TransactionInfo.StorageFeeHeader" = "STORAGE FEE"; "Wallet.TransactionInfo.OtherFeeHeader" = "TRANSACTION FEE"; -"Wallet.TransactionInfo.StorageFeeInfo" = "Blockchain validators collect a tiny fee for storing information about your decentralized wallet. [More info]()"; +"Wallet.TransactionInfo.StorageFeeInfo" = "Blockchain validators collect a tiny fee for storing information about your decentralized wallet and processing your transactions. [More info]()"; "Wallet.TransactionInfo.OtherFeeInfo" = "Blockchain validators collect a tiny fee for processing your decentralized transactions. [More info]()"; "Wallet.TransactionInfo.FeeInfoURL" = "https://telegram.org/wallet/fee"; "Wallet.WordCheck.Title" = "Test Time!"; @@ -4979,3 +4979,15 @@ Any member of this group will be able to see messages in the channel."; "Wallet.VoiceOver.Editing.ClearText" = "Clear text"; "Wallet.Receive.ShareInvoiceUrlInfo" = "Share this link with other Gram wallet owners to receive %@ Grams from them."; + +"Conversation.ClearCache" = "Clear Cache"; +"ClearCache.Description" = "Media files will be deleted from your phone, but available for re-downloading when necessary."; +"ClearCache.FreeSpaceDescription" = "If you want to save space on your device, you don't need to delete anything.\n\nYou can use cache settings to remove unnecessary media — and re-download files if you need them again."; +"ClearCache.FreeSpace" = "Free Space"; +"ClearCache.Success" = "**%@** freed on your %@!"; + +"Conversation.ScheduleMessage.SendWhenOnline" = "Send When Online"; +"ScheduledMessages.ScheduledOnline" = "Scheduled until online"; + +"Conversation.SwipeToReplyHintTitle" = "Swipe To Reply"; +"Conversation.SwipeToReplyHintText" = "Swipe left on any message to reply to it."; diff --git a/Wallet/Sources/AppDelegate.swift b/Wallet/Sources/AppDelegate.swift index 725c891b0b..138badccaf 100644 --- a/Wallet/Sources/AppDelegate.swift +++ b/Wallet/Sources/AppDelegate.swift @@ -366,7 +366,7 @@ private final class WalletContextImpl: NSObject, WalletContext, UIImagePickerCon } } - func pickImage(completion: @escaping (UIImage) -> Void) { + func pickImage(present: @escaping (ViewController) -> Void, completion: @escaping (UIImage) -> Void) { self.currentImagePickerCompletion = completion let pickerController = UIImagePickerController() @@ -465,6 +465,9 @@ private final class WalletContextImpl: NSObject, WalletContext, UIImagePickerCon buttonTextColor: .white, incomingFundsTitleColor: UIColor(rgb: 0x00b12c), outgoingFundsTitleColor: UIColor(rgb: 0xff3b30) + ), transaction: WalletTransactionTheme( + descriptionBackgroundColor: UIColor(rgb: 0xf1f1f4), + descriptionTextColor: .black ), setup: WalletSetupTheme( buttonFillColor: accentColor, buttonForegroundColor: .white, diff --git a/Watch/Extension/TGNotificationController.m b/Watch/Extension/TGNotificationController.m index b775ddfd15..55e5fd8ab2 100644 --- a/Watch/Extension/TGNotificationController.m +++ b/Watch/Extension/TGNotificationController.m @@ -389,6 +389,11 @@ completionBlock(); } +- (NSArray *)suggestionsForResponseToActionWithIdentifier:(NSString *)identifier forNotification:(UNNotification *)notification inputLanguage:(NSString *)inputLanguage +{ + return [TGInputController suggestionsForText:nil]; +} + - (NSArray *)suggestionsForResponseToActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)localNotification inputLanguage:(NSString *)inputLanguage { return [TGInputController suggestionsForText:nil]; diff --git a/Widget/Info.plist b/Widget/Info.plist index f3de288c2a..7e87bf7a10 100644 --- a/Widget/Info.plist +++ b/Widget/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - People + ${APP_NAME} CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index f7b2929105..1d54e5b997 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -302,7 +302,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, strongSelf.didShowProxyUnavailableTooltipController = true let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.Proxy_TooltipUnavailable), timeout: 60.0, dismissByTapOutside: true) strongSelf.proxyUnavailableTooltipController = tooltipController - tooltipController.dismissed = { [weak tooltipController] in + tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.proxyUnavailableTooltipController === tooltipController { strongSelf.proxyUnavailableTooltipController = nil } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 5742165831..ce343e995d 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -825,31 +825,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } - func foldLineBreaks(_ text: String, allowTwoLines: Bool) -> String { - var lines = text.split { $0.isNewline } - var startedBothLines = false - var result = "" - for line in lines { - if result.isEmpty { - result += line - } else { - if allowTwoLines && !startedBothLines { - result += "\n" + line - startedBothLines = true - } else { - result += " " + line - } - } - } - return result - } - let messageText: String if let currentChatListText = currentChatListText, currentChatListText.0 == text { messageText = currentChatListText.1 chatListText = currentChatListText } else { - messageText = foldLineBreaks(text, allowTwoLines: peerText == nil) + messageText = foldLineBreaks(text) chatListText = (text, messageText) } @@ -857,7 +838,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { hasDraft = true authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) - attributedText = NSAttributedString(string: foldLineBreaks(embeddedState.text.string.replacingOccurrences(of: "\n\n", with: " "), allowTwoLines: false), font: textFont, textColor: theme.messageTextColor) + attributedText = NSAttributedString(string: foldLineBreaks(embeddedState.text.string.replacingOccurrences(of: "\n\n", with: " ")), font: textFont, textColor: theme.messageTextColor) } else if let message = message { let composedString: NSMutableAttributedString if let inlineAuthorPrefix = inlineAuthorPrefix { @@ -1839,3 +1820,20 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } } + +private func foldLineBreaks(_ text: String) -> String { + var lines = text.split { $0.isNewline } + var startedBothLines = false + var result = "" + for line in lines { + if line.isEmpty { + continue + } + if result.isEmpty { + result += line + } else { + result += " " + line + } + } + return result +} diff --git a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift index dbd913a14b..db26283320 100644 --- a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift +++ b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift @@ -12,6 +12,8 @@ import AccountContext public enum DeleteChatPeerAction { case delete case clearHistory + case clearCache + case clearCacheSuggestion } public final class DeleteChatPeerActionSheetItem: ActionSheetItem { @@ -81,8 +83,20 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { self.avatarNode.setPeer(account: context.account, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: overrideImage) } - let text: (String, [(Int, NSRange)]) + var attributedText: NSAttributedString? switch action { + case .clearCache, .clearCacheSuggestion: + switch action { + case .clearCache: + attributedText = NSAttributedString(string: strings.ClearCache_Description, font: Font.regular(14.0), textColor: theme.primaryTextColor) + case .clearCacheSuggestion: + attributedText = NSAttributedString(string: strings.ClearCache_FreeSpaceDescription, font: Font.regular(14.0), textColor: theme.primaryTextColor) + default: + break + } + default: + var text: (String, [(Int, NSRange)])? + switch action { case .delete: if chatPeer.id == context.account.peerId { text = (strings.ChatList_DeleteSavedMessagesConfirmation, []) @@ -97,16 +111,24 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { } case .clearHistory: text = strings.ChatList_ClearChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder)) - } - let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: Font.regular(14.0), textColor: theme.primaryTextColor)) - for (_, range) in text.1 { - attributedText.addAttribute(.font, value: Font.semibold(14.0), range: range) + default: + break + } + if let text = text { + var formattedAttributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text.0, font: Font.regular(14.0), textColor: theme.primaryTextColor)) + for (_, range) in text.1 { + formattedAttributedText.addAttribute(.font, value: Font.semibold(14.0), range: range) + } + attributedText = formattedAttributedText + } } - self.textNode.attributedText = attributedText - - self.accessibilityArea.accessibilityLabel = attributedText.string - self.accessibilityArea.accessibilityTraits = .staticText + if let attributedText = attributedText { + self.textNode.attributedText = attributedText + + self.accessibilityArea.accessibilityLabel = attributedText.string + self.accessibilityArea.accessibilityTraits = .staticText + } } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { diff --git a/submodules/Display/Display/ActionSheetButtonItem.swift b/submodules/Display/Display/ActionSheetButtonItem.swift index b78ad09ac1..d5e66cb69e 100644 --- a/submodules/Display/Display/ActionSheetButtonItem.swift +++ b/submodules/Display/Display/ActionSheetButtonItem.swift @@ -8,7 +8,6 @@ public enum ActionSheetButtonColor { case disabled } - public enum ActionSheetButtonFont { case `default` case bold diff --git a/submodules/Display/Display/DeviceMetrics.swift b/submodules/Display/Display/DeviceMetrics.swift index 76ec247a9d..bc78143e2a 100644 --- a/submodules/Display/Display/DeviceMetrics.swift +++ b/submodules/Display/Display/DeviceMetrics.swift @@ -44,7 +44,14 @@ public enum DeviceMetrics: CaseIterable, Equatable { let additionalSize = CGSize(width: screenSize.width, height: screenSize.height + 20.0) for device in DeviceMetrics.allCases { if let _ = onScreenNavigationHeight, device.onScreenNavigationHeight(inLandscape: false) == nil { - continue + if case .tablet = device.type { + if screenSize.height == 1024.0 && screenSize.width == 768.0 { + } else { + continue + } + } else { + continue + } } let width = device.screenSize.width diff --git a/submodules/Display/Display/TooltipController.swift b/submodules/Display/Display/TooltipController.swift index bf2c377492..2b440b21b1 100644 --- a/submodules/Display/Display/TooltipController.swift +++ b/submodules/Display/Display/TooltipController.swift @@ -63,11 +63,12 @@ open class TooltipController: ViewController, StandalonePresentableController { public private(set) var content: TooltipControllerContent - open func updateContent(_ content: TooltipControllerContent, animated: Bool, extendTimer: Bool) { + open func updateContent(_ content: TooltipControllerContent, animated: Bool, extendTimer: Bool, arrowOnBottom: Bool = true) { if self.content != content { self.content = content if self.isNodeLoaded { self.controllerNode.updateText(self.content.text, transition: animated ? .animated(duration: 0.25, curve: .easeInOut) : .immediate) + self.controllerNode.arrowOnBottom = arrowOnBottom if extendTimer, self.timeoutTimer != nil { self.timeoutTimer?.invalidate() self.timeoutTimer = nil @@ -84,15 +85,17 @@ open class TooltipController: ViewController, StandalonePresentableController { private var timeoutTimer: SwiftSignalKit.Timer? private var layout: ContainerViewLayout? + private var initialArrowOnBottom: Bool - public var dismissed: (() -> Void)? + public var dismissed: ((Bool) -> Void)? - public init(content: TooltipControllerContent, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false) { + public init(content: TooltipControllerContent, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true) { self.content = content self.timeout = timeout self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource self.dismissImmediatelyOnLayoutUpdate = dismissImmediatelyOnLayoutUpdate + self.initialArrowOnBottom = arrowOnBottom super.init(navigationBarPresentationData: nil) @@ -108,9 +111,10 @@ open class TooltipController: ViewController, StandalonePresentableController { } override open func loadDisplayNode() { - self.displayNode = TooltipControllerNode(content: self.content, dismiss: { [weak self] in - self?.dismiss() + self.displayNode = TooltipControllerNode(content: self.content, dismiss: { [weak self] tappedInside in + self?.dismiss(tappedInside: tappedInside) }, dismissByTapOutside: self.dismissByTapOutside, dismissByTapOutsideSource: self.dismissByTapOutsideSource) + self.controllerNode.arrowOnBottom = self.initialArrowOnBottom self.displayNodeDidLoad() } @@ -154,7 +158,7 @@ open class TooltipController: ViewController, StandalonePresentableController { if self.timeoutTimer == nil { let timeoutTimer = SwiftSignalKit.Timer(timeout: self.timeout, repeat: false, completion: { [weak self] in if let strongSelf = self { - strongSelf.dismissed?() + strongSelf.dismissed?(false) strongSelf.controllerNode.animateOut { self?.presentingViewController?.dismiss(animated: false) } @@ -165,16 +169,20 @@ open class TooltipController: ViewController, StandalonePresentableController { } } - override open func dismiss(completion: (() -> Void)? = nil) { - self.dismissed?() + private func dismiss(tappedInside: Bool, completion: (() -> Void)? = nil) { + self.dismissed?(tappedInside) self.controllerNode.animateOut { [weak self] in - self?.presentingViewController?.dismiss(animated: false) - completion?() + self?.presentingViewController?.dismiss(animated: false) + completion?() } } + override open func dismiss(completion: (() -> Void)? = nil) { + self.dismiss(tappedInside: false, completion: completion) + } + open func dismissImmediately() { - self.dismissed?() + self.dismissed?(false) self.presentingViewController?.dismiss(animated: false) } } diff --git a/submodules/Display/Display/TooltipControllerNode.swift b/submodules/Display/Display/TooltipControllerNode.swift index 1723fc757e..3b39885dc0 100644 --- a/submodules/Display/Display/TooltipControllerNode.swift +++ b/submodules/Display/Display/TooltipControllerNode.swift @@ -3,7 +3,7 @@ import UIKit import AsyncDisplayKit final class TooltipControllerNode: ASDisplayNode { - private let dismiss: () -> Void + private let dismiss: (Bool) -> Void private var validLayout: ContainerViewLayout? @@ -19,7 +19,7 @@ final class TooltipControllerNode: ASDisplayNode { private var dismissedByTouchOutside = false private var dismissByTapOutsideSource = false - init(content: TooltipControllerContent, dismiss: @escaping () -> Void, dismissByTapOutside: Bool, dismissByTapOutsideSource: Bool) { + init(content: TooltipControllerContent, dismiss: @escaping (Bool) -> Void, dismissByTapOutside: Bool, dismissByTapOutsideSource: Bool) { self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource @@ -129,15 +129,16 @@ final class TooltipControllerNode: ASDisplayNode { eventIsPresses = event.type == .presses } if event.type == .touches || eventIsPresses { + let pointInside = self.containerNode.frame.contains(point) if self.containerNode.frame.contains(point) || self.dismissByTapOutside { if !self.dismissedByTouchOutside { self.dismissedByTouchOutside = true - self.dismiss() + self.dismiss(pointInside) } } else if self.dismissByTapOutsideSource, let sourceRect = self.sourceRect, !sourceRect.contains(point) { if !self.dismissedByTouchOutside { self.dismissedByTouchOutside = true - self.dismiss() + self.dismiss(false) } } return nil diff --git a/submodules/Display/Display/ViewController.swift b/submodules/Display/Display/ViewController.swift index 3d0aa3e717..cdaa03ea1d 100644 --- a/submodules/Display/Display/ViewController.swift +++ b/submodules/Display/Display/ViewController.swift @@ -455,6 +455,7 @@ public enum ViewControllerNavigationPresentation { } navigationController.filterController(self, animated: animated) } else { + self.presentingViewController?.dismiss(animated: false, completion: nil) assertionFailure() } } diff --git a/submodules/Display/Display/WindowContent.swift b/submodules/Display/Display/WindowContent.swift index a9b806d3d2..9145721a3d 100644 --- a/submodules/Display/Display/WindowContent.swift +++ b/submodules/Display/Display/WindowContent.swift @@ -479,6 +479,7 @@ public class Window1 { let screenHeight: CGFloat var inPopover = false if keyboardFrame.width.isEqual(to: UIScreen.main.bounds.width) { + let screenSize = UIScreen.main.bounds.size var portraitScreenSize = UIScreen.main.bounds.size if portraitScreenSize.width > portraitScreenSize.height { portraitScreenSize = CGSize(width: portraitScreenSize.height, height: portraitScreenSize.width) @@ -487,23 +488,35 @@ public class Window1 { if portraitLayoutSize.width > portraitLayoutSize.height { portraitLayoutSize = CGSize(width: portraitLayoutSize.height, height: portraitLayoutSize.width) } - if abs(strongSelf.windowLayout.size.height - UIScreen.main.bounds.height) > 41.0 { - if abs(portraitLayoutSize.height - portraitScreenSize.height) > 41.0 || abs(portraitLayoutSize.width - portraitScreenSize.width) > 41.0 { - popoverDelta = 48.0 + + if strongSelf.windowLayout.size.height != screenSize.height { + let heightDelta = screenSize.height - strongSelf.windowLayout.size.height + + let heightDeltaValid = heightDelta > 0.0 && heightDelta < 100.0 + + if heightDeltaValid { inPopover = true + popoverDelta = heightDelta / 2.0 + } + } + + if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { + screenHeight = UIScreen.main.bounds.height + } else { + screenHeight = strongSelf.windowLayout.size.height + } + + /*if abs(strongSelf.windowLayout.size.height - UIScreen.main.bounds.height) > 41.0 { + if abs(portraitLayoutSize.height - portraitScreenSize.height) > 41.0 || abs(portraitLayoutSize.width - portraitScreenSize.width) > 41.0 { screenHeight = strongSelf.windowLayout.size.height } else { screenHeight = UIScreen.main.bounds.height } } else if abs(strongSelf.windowLayout.size.height - UIScreen.main.bounds.height) > 39.0 { screenHeight = UIScreen.main.bounds.height - if abs(portraitLayoutSize.height - portraitScreenSize.height) > 39.0 || abs(portraitLayoutSize.width - portraitScreenSize.width) > 39.0 { - popoverDelta = 40.0 - inPopover = true - } } else { screenHeight = strongSelf.windowLayout.size.height - } + }*/ } else { screenHeight = UIScreen.main.bounds.width } @@ -513,11 +526,9 @@ public class Window1 { keyboardHeight = 0.0 } else { keyboardHeight = max(0.0, screenHeight - keyboardFrame.minY) - if inPopover { - if strongSelf.windowLayout.onScreenNavigationHeight != nil { - if !keyboardHeight.isZero { - keyboardHeight = max(0.0, keyboardHeight + popoverDelta / 2.0) - } + if inPopover && !keyboardHeight.isZero { + if #available(iOSApplicationExtension 13.0, iOS 13.0, *) { + keyboardHeight = max(0.0, keyboardHeight - popoverDelta) } else { keyboardHeight = max(0.0, keyboardHeight - popoverDelta) } @@ -1212,7 +1223,11 @@ public class Window1 { } private func collectScreenEdgeGestures() -> UIRectEdge { - var edges = self.presentationContext.combinedDeferScreenEdgeGestures() + var edges: UIRectEdge = [] + if let navigationController = self._rootController as? NavigationController, let overlayController = navigationController.topOverlayController { + edges = edges.union(overlayController.deferScreenEdgeGestures) + } + edges = edges.union(self.presentationContext.combinedDeferScreenEdgeGestures()) for controller in self.topLevelOverlayControllers { if let controller = controller as? ViewController { diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index a57a3d5fca..0c8ccdb12d 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -414,7 +414,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll self.requestLayout?(.immediate) } - self.deleteButton.isHidden = origin == nil + if origin == nil { + self.deleteButton.isHidden = true + } } func setMessage(_ message: Message) { @@ -831,7 +833,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll if !ask && items.count == 1 { let _ = deleteMessagesInteractively(postbox: strongSelf.context.account.postbox, messageIds: messages.map { $0.id }, type: .forEveryone).start() strongSelf.controllerInteraction?.dismissController() - } else { + } else if !items.isEmpty { actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationMapViewController.m b/submodules/LegacyComponents/LegacyComponents/TGLocationMapViewController.m index 73d55621ab..6703d330d7 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationMapViewController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationMapViewController.m @@ -179,7 +179,7 @@ const CGFloat TGLocationMapInset = 100.0f; return; [self updateMapHeightAnimated:false]; - _optionsView.frame = CGRectMake(self.view.bounds.size.width - 45.0f - 6.0f - self.controllerSafeAreaInset.right, self.controllerInset.top + 6.0f, 45.0f, 90.0f); + _optionsView.frame = CGRectMake(self.view.bounds.size.width - 45.0f - 6.0f - self.controllerSafeAreaInset.right, 56.0f + 6.0f, 45.0f, 90.0f); _tableView.contentOffset = CGPointMake(0.0f, -_tableViewTopInset - self.controllerInset.top); } @@ -259,7 +259,7 @@ const CGFloat TGLocationMapInset = 100.0f; [scrollView setContentOffset:contentOffset animated:false]; } - _optionsView.frame = CGRectMake(self.view.bounds.size.width - 45.0f - 6.0f, self.controllerInset.top + 6.0f, 45.0f, 90.0f); + _optionsView.frame = CGRectMake(self.view.bounds.size.width - 45.0f - 6.0f, 56.0f + 6.0f, 45.0f, 90.0f); } - (NSInteger)tableView:(UITableView *)__unused tableView numberOfRowsInSection:(NSInteger)__unused section diff --git a/submodules/LegacyComponents/LegacyComponents/TGLocationPickerController.m b/submodules/LegacyComponents/LegacyComponents/TGLocationPickerController.m index 5f308e5118..96800ff556 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGLocationPickerController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGLocationPickerController.m @@ -179,13 +179,13 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f }; [self.navigationController.view addSubview:_searchBarOverlay]; CGFloat safeAreaInset = self.controllerSafeAreaInset.top > FLT_EPSILON ? self.controllerSafeAreaInset.top : 0.0f; - _searchBarWrapper = [[UIView alloc] initWithFrame:CGRectMake(0, -44 - safeAreaInset, self.navigationController.view.frame.size.width, 44 + safeAreaInset)]; + _searchBarWrapper = [[UIView alloc] initWithFrame:CGRectMake(0, -44.0f, self.navigationController.view.frame.size.width, 44.0f)]; _searchBarWrapper.autoresizingMask = UIViewAutoresizingFlexibleWidth; _searchBarWrapper.backgroundColor = self.pallete != nil ? self.pallete.backgroundColor : [UIColor whiteColor]; _searchBarWrapper.hidden = true; [self.navigationController.view addSubview:_searchBarWrapper]; - _searchBar = [[TGSearchBar alloc] initWithFrame:CGRectMake(0.0f, safeAreaInset, _searchBarWrapper.frame.size.width, [TGSearchBar searchBarBaseHeight]) style:TGSearchBarStyleHeader]; + _searchBar = [[TGSearchBar alloc] initWithFrame:CGRectMake(0.0f, 0.0f, _searchBarWrapper.frame.size.width, [TGSearchBar searchBarBaseHeight]) style:TGSearchBarStyleHeader]; if (self.pallete != nil) [_searchBar setPallete:self.pallete.searchBarPallete]; _searchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth; @@ -808,10 +808,7 @@ const CGPoint TGLocationPickerPinOffset = { 0.0f, 33.0f }; } else { - frame.origin.y = 0; - if (self.navigationController.modalPresentationStyle == UIModalPresentationFormSheet) - frame.origin.y -= 20; - + frame.origin.y = 0; _searchBarOverlay.alpha = 1.0f; } _searchBarWrapper.frame = frame; diff --git a/submodules/LegacyComponents/LegacyComponents/TGModernGalleryController.m b/submodules/LegacyComponents/LegacyComponents/TGModernGalleryController.m index ec597512c5..d5e18176ca 100644 --- a/submodules/LegacyComponents/LegacyComponents/TGModernGalleryController.m +++ b/submodules/LegacyComponents/LegacyComponents/TGModernGalleryController.m @@ -1191,6 +1191,13 @@ static CGFloat transformRotation(CGAffineTransform transform) else [_model _transitionCompleted]; } + + if (!_previewMode) { + if (self.adjustsStatusBarVisibility && (!_showInterface || [_view.interfaceView prefersStatusBarHidden])) + { + [_context setApplicationStatusBarAlpha:0.0f]; + } + } } - (void)viewWillAppear:(BOOL)animated @@ -1203,14 +1210,6 @@ static CGFloat transformRotation(CGAffineTransform transform) if (!_showInterface) { _view.interfaceView.alpha = 0.0f; - - if (self.adjustsStatusBarVisibility) - [_context setApplicationStatusBarAlpha:0.0f]; - } - else if ([_view.interfaceView prefersStatusBarHidden]) - { - if (self.adjustsStatusBarVisibility) - [_context setApplicationStatusBarAlpha:0.0f]; } } diff --git a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift index 725051e022..9b409f1926 100644 --- a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift @@ -9,6 +9,8 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { private let audioFrame: FFMpegAVFrame private var resetDecoderOnNextFrame = true + private var delayedFrames: [MediaTrackFrame] = [] + init(codecContext: FFMpegAVCodecContext) { self.codecContext = codecContext self.audioFrame = FFMpegAVFrame() @@ -19,16 +21,58 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? { let status = frame.packet.send(toDecoder: self.codecContext) if status == 0 { - if self.codecContext.receive(into: self.audioFrame) { - return convertAudioFrame(self.audioFrame, pts: frame.pts, duration: frame.duration) + while self.codecContext.receive(into: self.audioFrame) { + if let convertedFrame = convertAudioFrame(self.audioFrame, pts: frame.pts, duration: frame.duration) { + self.delayedFrames.append(convertedFrame) + } + } + + if self.delayedFrames.count >= 1 { + var minFrameIndex = 0 + var minPosition = self.delayedFrames[0].position + for i in 1 ..< self.delayedFrames.count { + if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 { + minFrameIndex = i + minPosition = self.delayedFrames[i].position + } + } + return self.delayedFrames.remove(at: minFrameIndex) } } return nil } + func takeQueuedFrame() -> MediaTrackFrame? { + if self.delayedFrames.count >= 1 { + var minFrameIndex = 0 + var minPosition = self.delayedFrames[0].position + for i in 1 ..< self.delayedFrames.count { + if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 { + minFrameIndex = i + minPosition = self.delayedFrames[i].position + } + } + return self.delayedFrames.remove(at: minFrameIndex) + } else { + return nil + } + } + func takeRemainingFrame() -> MediaTrackFrame? { - return nil + if !self.delayedFrames.isEmpty { + var minFrameIndex = 0 + var minPosition = self.delayedFrames[0].position + for i in 1 ..< self.delayedFrames.count { + if CMTimeCompare(self.delayedFrames[i].position, minPosition) < 0 { + minFrameIndex = i + minPosition = self.delayedFrames[i].position + } + } + return self.delayedFrames.remove(at: minFrameIndex) + } else { + return nil + } } private func convertAudioFrame(_ frame: FFMpegAVFrame, pts: CMTime, duration: CMTime) -> MediaTrackFrame? { diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaPassthroughVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaPassthroughVideoFrameDecoder.swift index f417d3d40a..ef05414b40 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaPassthroughVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaPassthroughVideoFrameDecoder.swift @@ -39,6 +39,10 @@ final class FFMpegMediaPassthroughVideoFrameDecoder: MediaTrackFrameDecoder { return MediaTrackFrame(type: .video, sampleBuffer: sampleBuffer!, resetDecoder: resetDecoder, decoded: false, rotationAngle: self.rotationAngle) } + func takeQueuedFrame() -> MediaTrackFrame? { + return nil + } + func takeRemainingFrame() -> MediaTrackFrame? { return nil } diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index 0bfdb1731f..d77320d18a 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -87,6 +87,10 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { return nil } + public func takeQueuedFrame() -> MediaTrackFrame? { + return nil + } + public func takeRemainingFrame() -> MediaTrackFrame? { if !self.delayedFrames.isEmpty { var minFrameIndex = 0 diff --git a/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift b/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift index 08650acba8..210fc7ab2a 100644 --- a/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift +++ b/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift @@ -146,6 +146,10 @@ public final class MediaTrackFrameBuffer { } public func takeFrame() -> MediaTrackFrameResult { + if let decodedFrame = self.decoder.takeQueuedFrame() { + return .frame(decodedFrame) + } + if !self.frames.isEmpty { let frame = self.frames.removeFirst() if let decodedFrame = self.decoder.decode(frame: frame) { diff --git a/submodules/MediaPlayer/Sources/MediaTrackFrameDecoder.swift b/submodules/MediaPlayer/Sources/MediaTrackFrameDecoder.swift index 9e4c8266ec..0f27a3f02a 100644 --- a/submodules/MediaPlayer/Sources/MediaTrackFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/MediaTrackFrameDecoder.swift @@ -1,6 +1,7 @@ protocol MediaTrackFrameDecoder { func decode(frame: MediaTrackDecodableFrame) -> MediaTrackFrame? + func takeQueuedFrame() -> MediaTrackFrame? func takeRemainingFrame() -> MediaTrackFrame? func reset() } diff --git a/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift b/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift index cded7a46ef..50432817d9 100644 --- a/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift @@ -168,10 +168,6 @@ final class PasscodeEntryControllerNode: ASDisplayNode { } var size = validLayout.size - if case .compact = validLayout.metrics.widthClass, size.width > size.height { - size = CGSize(width: size.height, height: size.width) - } - if let background = self.background, background.size == size { return } @@ -334,36 +330,11 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.validLayout = layout self.updateBackground() - - if layout.size.width == 320.0 { - self.iconNode.alpha = 0.0 - } - + let bounds = CGRect(origin: CGPoint(), size: layout.size) transition.updateFrame(node: self.backgroundNode, frame: bounds) transition.updateFrame(view: self.effectView, frame: bounds) - let iconSize = CGSize(width: 35.0, height: 37.0) - transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0) + 6.0, y: layout.insets(options: .statusBar).top + 15.0), size: iconSize)) - - let passcodeLayout = PasscodeLayout(layout: layout) - - let inputFieldFrame = self.inputFieldNode.updateLayout(layout: passcodeLayout.layout, topOffset: passcodeLayout.inputFieldOffset, transition: transition) - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let titleSize = self.titleNode.updateLayout(layout: layout, transition: transition) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: passcodeLayout.titleOffset), size: titleSize)) - - var subtitleOffset = passcodeLayout.subtitleOffset - if case .alphanumeric = self.passcodeType { - subtitleOffset = 16.0 - } - let subtitleSize = self.subtitleNode.updateLayout(layout: layout, transition: transition) - transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: inputFieldFrame.maxY + subtitleOffset), size: subtitleSize)) - - let (keyboardFrame, keyboardButtonSize) = self.keyboardNode.updateLayout(layout: passcodeLayout, transition: transition) - transition.updateFrame(node: self.keyboardNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - switch self.passcodeType { case .digits6, .digits4: self.keyboardNode.alpha = 1.0 @@ -373,27 +344,104 @@ final class PasscodeEntryControllerNode: ASDisplayNode { self.deleteButtonNode.alpha = 0.0 } + let isLandscape = layout.orientation == .landscape && layout.deviceMetrics.type != .tablet + let keyboardHidden = self.keyboardNode.alpha == 0.0 + + let layoutSize: CGSize + if isLandscape { + if keyboardHidden { + layoutSize = CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: layout.size.height) + } else { + layoutSize = CGSize(width: layout.size.width / 2.0, height: layout.size.height) + } + } else { + layoutSize = layout.size + } + + if layout.size.width == 320.0 || (isLandscape && keyboardHidden) { + self.iconNode.alpha = 0.0 + } + + let passcodeLayout = PasscodeLayout(layout: layout) + let inputFieldOffset: CGFloat + if isLandscape { + let bottomInset = layout.inputHeight ?? 0.0 + if !keyboardHidden || bottomInset == 0.0 { + inputFieldOffset = floor(layoutSize.height / 2.0 + 12.0) + } else { + inputFieldOffset = floor(layoutSize.height - bottomInset) / 2.0 - 40.0 + } + } else { + inputFieldOffset = passcodeLayout.inputFieldOffset + } + + let inputFieldFrame = self.inputFieldNode.updateLayout(size: layoutSize, topOffset: inputFieldOffset, transition: transition) + transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left, y: 0.0), size: layoutSize)) + + let titleFrame: CGRect + if isLandscape { + let titleSize = self.titleNode.updateLayout(size: CGSize(width: layoutSize.width, height: layout.size.height), transition: transition) + titleFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: inputFieldFrame.minY - titleSize.height - 16.0), size: titleSize) + } else { + let titleSize = self.titleNode.updateLayout(size: layout.size, transition: transition) + titleFrame = CGRect(origin: CGPoint(x: 0.0, y: passcodeLayout.titleOffset), size: titleSize) + } + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + let iconSize = CGSize(width: 35.0, height: 37.0) + let iconFrame: CGRect + if isLandscape { + iconFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layoutSize.width - iconSize.width) / 2.0) + 6.0, y: titleFrame.minY - iconSize.height - 14.0), size: iconSize) + } else { + iconFrame = CGRect(origin: CGPoint(x: floor((layoutSize.width - iconSize.width) / 2.0) + 6.0, y: layout.insets(options: .statusBar).top + 15.0), size: iconSize) + } + transition.updateFrame(node: self.iconNode, frame: iconFrame) + + var subtitleOffset = passcodeLayout.subtitleOffset + if case .alphanumeric = self.passcodeType { + subtitleOffset = 16.0 + } + let subtitleSize = self.subtitleNode.updateLayout(size: layoutSize, transition: transition) + transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left, y: inputFieldFrame.maxY + subtitleOffset), size: subtitleSize)) + + let (keyboardFrame, keyboardButtonSize) = self.keyboardNode.updateLayout(layout: passcodeLayout, transition: transition) + transition.updateFrame(node: self.keyboardNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)) + let bottomInset = layout.inputHeight ?? 0.0 let cancelSize = self.cancelButtonNode.measure(layout.size) - var cancelY: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - cancelSize.height - passcodeLayout.keyboard.deleteOffset - if bottomInset > 0 && self.keyboardNode.alpha < 1.0 { - cancelY = layout.size.height - bottomInset - cancelSize.height - 20.0 + var bottomButtonY = layout.size.height - layout.intrinsicInsets.bottom - cancelSize.height - passcodeLayout.keyboard.deleteOffset + var cancelX = floor(keyboardFrame.minX + keyboardButtonSize.width / 2.0 - cancelSize.width / 2.0) + var cancelY = bottomButtonY + if bottomInset > 0 && keyboardHidden { + cancelX = floor((layout.size.width - cancelSize.width) / 2.0) + cancelY = layout.size.height - bottomInset - cancelSize.height - 15.0 - layout.intrinsicInsets.bottom + } else if isLandscape { + bottomButtonY = keyboardFrame.maxY - keyboardButtonSize.height + floor((keyboardButtonSize.height - cancelSize.height) / 2.0) + cancelY = bottomButtonY } - transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: floor(keyboardFrame.minX + keyboardButtonSize.width / 2.0 - cancelSize.width / 2.0), y: cancelY), size: cancelSize)) + transition.updateFrame(node: self.cancelButtonNode, frame: CGRect(origin: CGPoint(x: cancelX, y: cancelY), size: cancelSize)) let deleteSize = self.deleteButtonNode.measure(layout.size) - transition.updateFrame(node: self.deleteButtonNode, frame: CGRect(origin: CGPoint(x: floor(keyboardFrame.maxX - keyboardButtonSize.width / 2.0 - deleteSize.width / 2.0), y: layout.size.height - layout.intrinsicInsets.bottom - deleteSize.height - passcodeLayout.keyboard.deleteOffset), size: deleteSize)) + transition.updateFrame(node: self.deleteButtonNode, frame: CGRect(origin: CGPoint(x: floor(keyboardFrame.maxX - keyboardButtonSize.width / 2.0 - deleteSize.width / 2.0), y: bottomButtonY), size: deleteSize)) if let biometricIcon = self.biometricButtonNode.image(for: .normal) { + var biometricX = layout.safeInsets.left + floor((layoutSize.width - biometricIcon.size.width) / 2.0) var biometricY: CGFloat = 0.0 - if bottomInset > 0 && self.keyboardNode.alpha < 1.0 { - biometricY = inputFieldFrame.maxY + floor((layout.size.height - bottomInset - inputFieldFrame.maxY - biometricIcon.size.height) / 2.0) + if isLandscape { + if bottomInset > 0 && keyboardHidden { + biometricX = cancelX + cancelSize.width + 64.0 + } + biometricY = cancelY + floor((cancelSize.height - biometricIcon.size.height) / 2.0) } else { - biometricY = keyboardFrame.maxY + passcodeLayout.keyboard.biometricsOffset + if bottomInset > 0 && keyboardHidden { + biometricY = inputFieldFrame.maxY + floor((layout.size.height - bottomInset - inputFieldFrame.maxY - biometricIcon.size.height) / 2.0) + } else { + biometricY = keyboardFrame.maxY + passcodeLayout.keyboard.biometricsOffset + } } - transition.updateFrame(node: self.biometricButtonNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - biometricIcon.size.width) / 2.0), y: biometricY), size: biometricIcon.size)) + transition.updateFrame(node: self.biometricButtonNode, frame: CGRect(origin: CGPoint(x: biometricX, y: biometricY), size: biometricIcon.size)) } } } diff --git a/submodules/PasscodeUI/Sources/PasscodeEntryKeyboardNode.swift b/submodules/PasscodeUI/Sources/PasscodeEntryKeyboardNode.swift index c0b29833e7..2ac3624e48 100644 --- a/submodules/PasscodeUI/Sources/PasscodeEntryKeyboardNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeEntryKeyboardNode.swift @@ -58,7 +58,7 @@ private func generateButtonImage(background: PasscodeBackground, frame: CGRect, titleOffset = -11.0 } subtitleOffset = -54.0 - } else { + } else if size.width > 70.0 { titleFont = regularTitleFont subtitleFont = regularSubtitleFont if subtitle.isEmpty { @@ -68,6 +68,16 @@ private func generateButtonImage(background: PasscodeBackground, frame: CGRect, } subtitleOffset = -48.0 } + else { + titleFont = regularTitleFont + subtitleFont = regularSubtitleFont + if subtitle.isEmpty { + titleOffset = -11.0 + } else { + titleOffset = -4.0 + } + subtitleOffset = -41.0 + } let titlePath = CGMutablePath() titlePath.addRect(bounds.offsetBy(dx: 0.0, dy: titleOffset)) @@ -236,36 +246,66 @@ final class PasscodeEntryKeyboardNode: ASDisplayNode { } func updateLayout(layout: PasscodeLayout, transition: ContainedViewLayoutTransition) -> (CGRect, CGSize) { - let origin = CGPoint(x: floor((layout.layout.size.width - layout.keyboard.size.width) / 2.0), y: layout.keyboard.topOffset) + let origin: CGPoint + let buttonSize: CGFloat + let horizontalSecond: CGFloat + let horizontalThird: CGFloat + let verticalSecond: CGFloat + let verticalThird: CGFloat + let verticalFourth: CGFloat + let keyboardSize: CGSize + + if layout.layout.orientation == .landscape && layout.layout.deviceMetrics.type != .tablet { + let horizontalSpacing: CGFloat = 20.0 + let verticalSpacing: CGFloat = 12.0 + buttonSize = 65.0 + keyboardSize = CGSize(width: buttonSize * 3.0 + horizontalSpacing * 2.0, height: buttonSize * 4.0 + verticalSpacing * 3.0) + horizontalSecond = buttonSize + horizontalSpacing + horizontalThird = buttonSize * 2.0 + horizontalSpacing * 2.0 + verticalSecond = buttonSize + verticalSpacing + verticalThird = buttonSize * 2.0 + verticalSpacing * 2.0 + verticalFourth = buttonSize * 3.0 + verticalSpacing * 3.0 + origin = CGPoint(x: floor(layout.layout.size.width / 2.0 + (layout.layout.size.width / 2.0 - keyboardSize.width) / 2.0) - layout.layout.safeInsets.right, y: floor((layout.layout.size.height - keyboardSize.height) / 2.0)) + } else { + origin = CGPoint(x: floor((layout.layout.size.width - layout.keyboard.size.width) / 2.0), y: layout.keyboard.topOffset) + buttonSize = layout.keyboard.buttonSize + horizontalSecond = layout.keyboard.horizontalSecond + horizontalThird = layout.keyboard.horizontalThird + verticalSecond = layout.keyboard.verticalSecond + verticalThird = layout.keyboard.verticalThird + verticalFourth = layout.keyboard.verticalFourth + keyboardSize = layout.keyboard.size + } + if let subnodes = self.subnodes { for i in 0 ..< subnodes.count { var origin = origin if i % 3 == 0 { origin.x += 0.0 } else if (i % 3 == 1) { - origin.x += layout.keyboard.horizontalSecond + origin.x += horizontalSecond } else { - origin.x += layout.keyboard.horizontalThird + origin.x += horizontalThird } if i / 3 == 0 { origin.y += 0.0 } else if i / 3 == 1 { - origin.y += layout.keyboard.verticalSecond + origin.y += verticalSecond } else if i / 3 == 2 { - origin.y += layout.keyboard.verticalThird + origin.y += verticalThird } else if i / 3 == 3 { - origin.x += layout.keyboard.horizontalSecond - origin.y += layout.keyboard.verticalFourth + origin.x += horizontalSecond + origin.y += verticalFourth } - transition.updateFrame(node: subnodes[i], frame: CGRect(origin: origin, size: CGSize(width: layout.keyboard.buttonSize, height: layout.keyboard.buttonSize))) + transition.updateFrame(node: subnodes[i], frame: CGRect(origin: origin, size: CGSize(width: buttonSize, height: buttonSize))) } } - return (CGRect(origin: origin, size: layout.keyboard.size), CGSize(width: layout.keyboard.buttonSize, height: layout.keyboard.buttonSize)) + return (CGRect(origin: origin, size: keyboardSize), CGSize(width: buttonSize, height: buttonSize)) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/submodules/PasscodeUI/Sources/PasscodeEntryLabelNode.swift b/submodules/PasscodeUI/Sources/PasscodeEntryLabelNode.swift index ec6a09bad2..b77b0f829a 100644 --- a/submodules/PasscodeUI/Sources/PasscodeEntryLabelNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeEntryLabelNode.swift @@ -13,7 +13,7 @@ final class PasscodeEntryLabelNode: ASDisplayNode { private let wrapperNode: ASDisplayNode private let textNode: ASTextNode - private var validLayout: ContainerViewLayout? + private var validLayout: CGSize? override init() { self.wrapperNode = ASDisplayNode() @@ -34,13 +34,13 @@ final class PasscodeEntryLabelNode: ASDisplayNode { self.textNode.attributedText = text completion() - if let validLayout = self.validLayout { - let _ = self.updateLayout(layout: validLayout, transition: .immediate) + if let size = self.validLayout { + let _ = self.updateLayout(size: size, transition: .immediate) } case .slideIn: self.textNode.attributedText = text - if let validLayout = self.validLayout { - let _ = self.updateLayout(layout: validLayout, transition: .immediate) + if let size = self.validLayout { + let _ = self.updateLayout(size: size, transition: .immediate) } let offset = self.wrapperNode.bounds.width / 2.0 @@ -61,29 +61,29 @@ final class PasscodeEntryLabelNode: ASDisplayNode { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { _ in completion() }) - if let validLayout = self.validLayout { - let _ = self.updateLayout(layout: validLayout, transition: .immediate) + if let size = self.validLayout { + let _ = self.updateLayout(size: size, transition: .immediate) } }) } else { self.textNode.attributedText = text self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) completion() - if let validLayout = self.validLayout { - let _ = self.updateLayout(layout: validLayout, transition: .immediate) + if let size = self.validLayout { + let _ = self.updateLayout(size: size, transition: .immediate) } } } } - func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGSize { - self.validLayout = layout + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + self.validLayout = size - let textSize = self.textNode.measure(layout.size) - let textFrame = CGRect(x: floor((layout.size.width - textSize.width) / 2.0), y: 0.0, width: textSize.width, height: textSize.height) + let textSize = self.textNode.measure(size) + let textFrame = CGRect(x: floor((size.width - textSize.width) / 2.0), y: 0.0, width: textSize.width, height: textSize.height) transition.updateFrame(node: self.wrapperNode, frame: textFrame) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(), size: textSize)) - return CGSize(width: layout.size.width, height: 25.0) + return CGSize(width: size.width, height: 25.0) } } diff --git a/submodules/PasscodeUI/Sources/PasscodeInputFieldNode.swift b/submodules/PasscodeUI/Sources/PasscodeInputFieldNode.swift index 8e899a6421..dd2ce400ac 100644 --- a/submodules/PasscodeUI/Sources/PasscodeInputFieldNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeInputFieldNode.swift @@ -139,7 +139,7 @@ public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate { private let borderNode: ASImageNode private let dotNodes: [PasscodeEntryDotNode] - private var validLayout: (ContainerViewLayout, CGFloat)? + private var validLayout: (CGSize, CGFloat)? public var complete: ((String) -> Void)? @@ -202,15 +202,15 @@ public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.textFieldNode.textField.keyboardType = self.fieldType.keyboardType - if let (layout, topOffset) = self.validLayout { - let _ = self.updateLayout(layout: layout, topOffset: topOffset, transition: animated ? .animated(duration: 0.25, curve: .easeInOut) : .immediate) + if let (size, topOffset) = self.validLayout { + let _ = self.updateLayout(size: size, topOffset: topOffset, transition: animated ? .animated(duration: 0.25, curve: .easeInOut) : .immediate) } } func updateBackground(_ image: UIImage, size: CGSize) { self.background = (image, size) - if let (layout, topOffset) = self.validLayout { - let _ = self.updateLayout(layout: layout, topOffset: topOffset, transition: .immediate) + if let (size, topOffset) = self.validLayout { + let _ = self.updateLayout(size: size, topOffset: topOffset, transition: .immediate) } } @@ -302,13 +302,13 @@ public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate { self.textFieldNode.textField.text = "" } self.fieldType = fieldType - if let (layout, topOffset) = self.validLayout { - let _ = self.updateLayout(layout: layout, topOffset: topOffset, transition: .immediate) + if let (size, topOffset) = self.validLayout { + let _ = self.updateLayout(size: size, topOffset: topOffset, transition: .immediate) } } - public func updateLayout(layout: ContainerViewLayout, topOffset: CGFloat, transition: ContainedViewLayoutTransition) -> CGRect { - self.validLayout = (layout, topOffset) + public func updateLayout(size: CGSize, topOffset: CGFloat, transition: ContainedViewLayoutTransition) -> CGRect { + self.validLayout = (size, topOffset) let fieldAlpha: CGFloat switch self.fieldType { @@ -321,7 +321,7 @@ public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate { transition.updateAlpha(node: self.textFieldNode, alpha: fieldAlpha) transition.updateAlpha(node: self.borderNode, alpha: fieldAlpha) - let origin = CGPoint(x: floor((layout.size.width - dotDiameter * 6 - dotSpacing * 5) / 2.0), y: topOffset) + let origin = CGPoint(x: floor((size.width - dotDiameter * 6 - dotSpacing * 5) / 2.0), y: topOffset) for i in 0 ..< self.dotNodes.count { let node = self.dotNodes[i] let dotAlpha: CGFloat @@ -343,7 +343,7 @@ public final class PasscodeInputFieldNode: ASDisplayNode, UITextFieldDelegate { if !self.useCustomNumpad { inset = 16.0 } - let fieldFrame = CGRect(x: inset, y: origin.y, width: layout.size.width - inset * 2.0, height: fieldHeight) + let fieldFrame = CGRect(x: inset, y: origin.y, width: size.width - inset * 2.0, height: fieldHeight) transition.updateFrame(node: self.borderNode, frame: fieldFrame) transition.updateFrame(node: self.textFieldNode, frame: fieldFrame.insetBy(dx: 13.0, dy: 0.0)) if let (backgroundImage, backgroundSize) = self.background { diff --git a/submodules/PasscodeUI/Sources/PasscodeLayout.swift b/submodules/PasscodeUI/Sources/PasscodeLayout.swift index 90a8c918bf..d9ea84cebe 100644 --- a/submodules/PasscodeUI/Sources/PasscodeLayout.swift +++ b/submodules/PasscodeUI/Sources/PasscodeLayout.swift @@ -101,7 +101,7 @@ struct PasscodeKeyboardLayout { self.verticalThird = 176.0 self.verticalFourth = 264.0 self.size = CGSize(width: 265.0, height: 339.0) - self.topOffset = 0.0 + self.topOffset = 120.0 + (layout.size.height - self.size.height - 120.0) / 2.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 } diff --git a/submodules/PasscodeUI/Sources/PasscodeSetupControllerNode.swift b/submodules/PasscodeUI/Sources/PasscodeSetupControllerNode.swift index 9a88badb0a..d428ff9aee 100644 --- a/submodules/PasscodeUI/Sources/PasscodeSetupControllerNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeSetupControllerNode.swift @@ -147,7 +147,7 @@ final class PasscodeSetupControllerNode: ASDisplayNode { self.wrapperNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - let inputFieldFrame = self.inputFieldNode.updateLayout(layout: layout, topOffset: floor(insets.top + navigationBarHeight + (layout.size.height - navigationBarHeight - insets.top - insets.bottom - 24.0) / 2.0), transition: transition) + let inputFieldFrame = self.inputFieldNode.updateLayout(size: layout.size, topOffset: floor(insets.top + navigationBarHeight + (layout.size.height - navigationBarHeight - insets.top - insets.bottom - 24.0) / 2.0), transition: transition) transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.inputFieldBackgroundNode, frame: CGRect(x: 0.0, y: inputFieldFrame.minY - 6.0, width: layout.size.width, height: 48.0)) diff --git a/submodules/PeerInfoUI/Sources/ChannelInfoController.swift b/submodules/PeerInfoUI/Sources/ChannelInfoController.swift index 738c14c747..02d8c73d89 100644 --- a/submodules/PeerInfoUI/Sources/ChannelInfoController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelInfoController.swift @@ -893,6 +893,8 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi }, reportChannel: { presentControllerImpl?(peerReportOptionsController(context: context, subject: .peer(peerId), present: { c, a in presentControllerImpl?(c, a) + }, push: { c in + pushControllerImpl?(c) }, completion: { _ in }), nil) }, leaveChannel: { let _ = (context.account.postbox.transaction { transaction -> Peer? in diff --git a/submodules/PeerInfoUI/Sources/GroupInfoController.swift b/submodules/PeerInfoUI/Sources/GroupInfoController.swift index bdb0a057b6..dacc20697c 100644 --- a/submodules/PeerInfoUI/Sources/GroupInfoController.swift +++ b/submodules/PeerInfoUI/Sources/GroupInfoController.swift @@ -2072,7 +2072,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: }, sendLiveLocation: { _, _ in }, theme: presentationData.theme, customLocationPicker: true, presentationCompleted: { clearHighlightImpl?() }) - presentControllerImpl?(controller, nil) + pushControllerImpl?(controller) }) }, displayLocationContextMenu: { text in displayCopyContextMenuImpl?(text, .location) diff --git a/submodules/PeerInfoUI/Sources/PeerReportController.swift b/submodules/PeerInfoUI/Sources/PeerReportController.swift index 4996d5fb91..759c2d8d84 100644 --- a/submodules/PeerInfoUI/Sources/PeerReportController.swift +++ b/submodules/PeerInfoUI/Sources/PeerReportController.swift @@ -92,7 +92,7 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro }) } } else { - parent?.present(peerReportController(context: context, subject: subject, completion: completion), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + parent?.push(peerReportController(context: context, subject: subject, completion: completion)) } f(.dismissWithoutContent) }))) @@ -103,11 +103,13 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro parent.view.endEditing(true) parent.present(peerReportOptionsController(context: context, subject: subject, present: { [weak parent] c, a in parent?.present(c, in: .window(.root), with: a) + }, push: { [weak parent] c in + parent?.push(c) }, completion: completion), in: .window(.root)) } } -public func peerReportOptionsController(context: AccountContext, subject: PeerReportSubject, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (Bool) -> Void) -> ViewController { +public func peerReportOptionsController(context: AccountContext, subject: PeerReportSubject, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping (Bool) -> Void) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme)) @@ -170,7 +172,7 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe }) } } else { - controller?.present(peerReportController(context: context, subject: subject, completion: completion), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + push(peerReportController(context: context, subject: subject, completion: completion)) } controller?.dismissAnimated() @@ -349,6 +351,7 @@ private func peerReportController(context: AccountContext, subject: PeerReportSu } let controller = ItemListController(context: context, state: signal) + controller.navigationPresentation = .modal presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } diff --git a/submodules/PeerInfoUI/Sources/UserInfoController.swift b/submodules/PeerInfoUI/Sources/UserInfoController.swift index 6664479ff8..bc8fc5f2bc 100644 --- a/submodules/PeerInfoUI/Sources/UserInfoController.swift +++ b/submodules/PeerInfoUI/Sources/UserInfoController.swift @@ -1181,6 +1181,8 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe }, report: { presentControllerImpl?(peerReportOptionsController(context: context, subject: .peer(peerId), present: { c, a in presentControllerImpl?(c, a) + }, push: { c in + pushControllerImpl?(c) }, completion: { _ in }), nil) }) @@ -1579,8 +1581,6 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Pe let text: String = presentationData.strings.UserInfo_TapToCall let tooltipController = TooltipController(content: .text(text), dismissByTapOutside: true) - tooltipController.dismissed = { - } controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in if let resultItemNode = resultItemNode { return (resultItemNode, callButtonFrame) diff --git a/submodules/Postbox/Postbox/MessageHistoryTable.swift b/submodules/Postbox/Postbox/MessageHistoryTable.swift index b650a789b3..5f40bd055b 100644 --- a/submodules/Postbox/Postbox/MessageHistoryTable.swift +++ b/submodules/Postbox/Postbox/MessageHistoryTable.swift @@ -2479,18 +2479,23 @@ final class MessageHistoryTable: Table { } } - func enumerateMedia(lowerBound: MessageIndex?, limit: Int) -> ([PeerId: Set], [MediaId: Media], MessageIndex?) { + func enumerateMedia(lowerBound: MessageIndex?, upperBound: MessageIndex?, limit: Int) -> ([PeerId: Set], [MediaId: Media], MessageIndex?) { var mediaRefs: [MediaId: Media] = [:] var result: [PeerId: Set] = [:] var lastIndex: MessageIndex? var count = 0 - self.valueBox.range(self.table, start: self.key(lowerBound == nil ? MessageIndex.absoluteLowerBound() : lowerBound!), end: self.key(MessageIndex.absoluteUpperBound()), values: { key, value in + self.valueBox.range(self.table, start: self.key(lowerBound == nil ? MessageIndex.absoluteLowerBound() : lowerBound!), end: self.key(upperBound == nil ? MessageIndex.absoluteUpperBound() : upperBound!), values: { key, value in count += 1 let entry = self.readIntermediateEntry(key, value: value) lastIndex = entry.message.index let message = entry.message + + if let upperBound = upperBound, message.id.peerId != upperBound.id.peerId { + return true + } + var parsedMedia: [Media] = [] let embeddedMediaData = message.embeddedMediaData.sharedBufferNoCopy() diff --git a/submodules/Postbox/Postbox/Postbox.swift b/submodules/Postbox/Postbox/Postbox.swift index bf8ce7f49a..d9de5d0a43 100644 --- a/submodules/Postbox/Postbox/Postbox.swift +++ b/submodules/Postbox/Postbox/Postbox.swift @@ -763,10 +763,10 @@ public final class Transaction { } } - public func enumerateMedia(lowerBound: MessageIndex?, limit: Int) -> ([PeerId: Set], [MediaId: Media], MessageIndex?) { + public func enumerateMedia(lowerBound: MessageIndex?, upperBound: MessageIndex?, limit: Int) -> ([PeerId: Set], [MediaId: Media], MessageIndex?) { assert(!self.disposed) if let postbox = self.postbox { - return postbox.messageHistoryTable.enumerateMedia(lowerBound: lowerBound, limit: limit) + return postbox.messageHistoryTable.enumerateMedia(lowerBound: lowerBound, upperBound: upperBound, limit: limit) } else { return ([:], [:], nil) } diff --git a/submodules/QrCode/Sources/QrCode.swift b/submodules/QrCode/Sources/QrCode.swift index a5b350c26a..6916f5c9cf 100644 --- a/submodules/QrCode/Sources/QrCode.swift +++ b/submodules/QrCode/Sources/QrCode.swift @@ -45,29 +45,29 @@ public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = n |> map { data, size, bytesPerRow in return { arguments in let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) - + let side = floorToScreenPixels(arguments.drawingSize.width / CGFloat(size)) + let padding: CGFloat = floor((arguments.drawingSize.width - CGFloat(side * CGFloat(size))) / 2.0) + + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + let codeScale: CGFloat = 1.10256 + let clipSide = fittedRect.width * 0.23124 + let clipRect = CGRect(x: fittedRect.midX - clipSide / 2.0, y: fittedRect.midY - clipSide / 2.0, width: clipSide, height: clipSide) + let cutout: (Int, Int)? if case .none = icon { cutout = nil } else { - switch size { - case 39: - cutout = (14, 24) - case 43: - cutout = (15, 27) - case 47: - cutout = (17, 29) - case 51: - cutout = (19, 31) - case 55: - cutout = (21, 33) - case 59: - cutout = (22, 36) - case 63: - cutout = (23, 39) - default: - cutout = (16, 26) + var cutoutSize = Int(ceil((clipSide + side * 2.0) / side)) + if size == 39 { + cutoutSize = 11 + } else if cutoutSize % 2 == 0 { + cutoutSize += 1 } + let start = (size - cutoutSize) / 2 + cutout = (start, start + cutoutSize - 1) } func valueAt(x: Int, y: Int) -> Bool { if x >= 0 && x < size && y >= 0 && y < size { @@ -87,9 +87,6 @@ public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = n } } - let side = floorToScreenPixels(arguments.drawingSize.width / CGFloat(size)) - let padding: CGFloat = floor((arguments.drawingSize.width - CGFloat(side * CGFloat(size))) / 2.0) - let squareSize = CGSize(width: side, height: side) let tmpContext = DrawingContext(size: CGSize(width: squareSize.width * 4.0, height: squareSize.height), scale: arguments.scale ?? 0.0, clear: true) tmpContext.withContext { c in @@ -233,14 +230,7 @@ public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = n c.translateBy(x: -padding, y: -padding) - let drawingRect = arguments.drawingRect - let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - - let codeScale: CGFloat = 43.0 / 39.0 - let clipSide = 56.0 * fittedRect.width / 267.0 * codeScale - let clipRect = CGRect(x: fittedRect.midX - clipSide / 2.0, y: fittedRect.midY - clipSide / 2.0, width: clipSide, height: clipSide) - + switch icon { case .proxy: let iconScale = fittedRect.width / 420.0 * codeScale diff --git a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift index 14765944f2..ba95b378b7 100644 --- a/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift +++ b/submodules/SegmentedControlNode/Sources/SegmentedControlNode.swift @@ -49,7 +49,7 @@ public final class SegmentedControlTheme: Equatable { public extension SegmentedControlTheme { convenience init(theme: PresentationTheme) { - self.init(backgroundColor: theme.rootController.navigationSearchBar.inputFillColor, foregroundColor: theme.rootController.navigationBar.backgroundColor, shadowColor: .black, textColor: theme.rootController.navigationBar.primaryTextColor, dividerColor: theme.list.freeInputField.strokeColor) + self.init(backgroundColor: theme.rootController.navigationBar.segmentedBackgroundColor, foregroundColor: theme.rootController.navigationBar.segmentedForegroundColor, shadowColor: .black, textColor: theme.rootController.navigationBar.segmentedTextColor, dividerColor: theme.rootController.navigationBar.segmentedDividerColor) } } diff --git a/submodules/SettingsUI/BUCK b/submodules/SettingsUI/BUCK index 46797f1273..b11d491874 100644 --- a/submodules/SettingsUI/BUCK +++ b/submodules/SettingsUI/BUCK @@ -81,6 +81,8 @@ static_library( "//submodules/WalletUI:WalletUI", "//submodules/Markdown:Markdown", "//submodules/PhoneNumberFormat:PhoneNumberFormat", + "//submodules/UndoUI:UndoUI", + "//submodules/DeleteChatPeerActionSheetItem:DeleteChatPeerActionSheetItem", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index b3728cb7eb..424e0b6206 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift @@ -185,7 +185,7 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { } case .shareProxyList: switch rhs { - case .enabled, .serversHeader, .addServer, .server, .useForCalls: + case .enabled, .serversHeader, .addServer, .server, .shareProxyList: return false default: return true diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift index 62f1cd7592..e6772908f7 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift @@ -12,6 +12,8 @@ import PresentationDataUtils import OverlayStatusController import AccountContext import ItemListPeerItem +import DeleteChatPeerActionSheetItem +import UndoUI private final class StorageUsageControllerArguments { let account: Account @@ -285,7 +287,7 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals return cacheSettings }) - var presentControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController, Any?) -> Void)? let statsPromise = Promise() let resetStats: () -> Void = { @@ -339,7 +341,7 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals ActionSheetItemGroup(items: timeoutItems), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) - presentControllerImpl?(controller) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openClearAll: { let _ = (statsPromise.get() |> take(1) @@ -506,7 +508,7 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) - presentControllerImpl?(controller) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() @@ -530,6 +532,8 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals clearDisposable.set((signal |> deliverOnMainQueue).start(completed: { statsPromise.set(.single(.result(resultStats))) + let deviceName = UIDevice.current.userInterfaceIdiom == .pad ? "iPad" : "iPhone" + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", deviceName).0), elevatedLayout: false, action: { _ in }), nil) })) } @@ -540,7 +544,7 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) - presentControllerImpl?(controller) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } }) @@ -548,8 +552,8 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals let _ = (statsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak statsPromise] result in if let result = result, case let .result(stats) = result { var additionalPeerId: PeerId? - if var categories = stats.media[peerId] { - if let channel = stats.peers[peerId] as? TelegramChannel, case .group = channel.info { + if var categories = stats.media[peerId], let peer = stats.peers[peerId] { + if let channel = peer as? TelegramChannel, case .group = channel.info { for (_, peer) in stats.peers { if let group = peer as? TelegramGroup, let migrationReference = group.migrationReference, migrationReference.peerId == peerId { if let additionalCategories = stats.media[group.id] { @@ -606,6 +610,8 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals } var items: [ActionSheetItem] = [] + items.append(DeleteChatPeerActionSheetItem(context: context, peer: peer, chatPeer: peer, action: .clearCache, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder)) + let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file] var totalSize: Int64 = 0 @@ -681,7 +687,7 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) - presentControllerImpl?(controller) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() @@ -705,6 +711,8 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals clearDisposable.set((signal |> deliverOnMainQueue).start(completed: { statsPromise.set(.single(.result(resultStats))) + let deviceName = UIDevice.current.userInterfaceIdiom == .pad ? "iPad" : "iPhone" + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", deviceName).0), elevatedLayout: false, action: { _ in }), nil) })) } @@ -715,7 +723,7 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) - presentControllerImpl?(controller) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } } } @@ -739,8 +747,8 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals } let controller = ItemListController(context: context, state: signal) - presentControllerImpl = { [weak controller] c in - controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) } dismissImpl = { [weak controller] in controller?.dismiss() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift index adaf4a03a7..cdf50e4be4 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/ForwardPrivacyChatPreviewItem.swift @@ -283,16 +283,11 @@ class ForwardPrivacyChatPreviewItemNode: ListViewItemNode { let horizontalOrigin: CGFloat = floor(min(max(8.0, sourceRect.midX - contentSize.width / 2.0), layout.size.width - contentSize.width - 8.0)) strongSelf.tooltipContainerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: contentSize) - //transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: contentSize)) strongSelf.tooltipContainerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom) strongSelf.tooltipContainerNode.updateLayout(transition: .immediate) - let textFrame = CGRect(origin: CGPoint(x: 6.0, y: 17.0), size: textSize) -// if transition.isAnimated, textFrame.size != self.textNode.frame.size { -// transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: textFrame.minX - self.textNode.frame.minX, y: 0.0)) -// } - + let textFrame = CGRect(origin: CGPoint(x: 6.0, y: 17.0), size: textSize) strongSelf.textNode.frame = textFrame } }) diff --git a/submodules/ShareItems/Sources/TGItemProviderSignals.m b/submodules/ShareItems/Sources/TGItemProviderSignals.m index cc4280fed6..6a7ae46a41 100644 --- a/submodules/ShareItems/Sources/TGItemProviderSignals.m +++ b/submodules/ShareItems/Sources/TGItemProviderSignals.m @@ -198,16 +198,43 @@ static CGSize TGFitSize(CGSize size, CGSize maxSize) { } else { if ([(NSObject *)item respondsToSelector:@selector(absoluteString)]) { NSURL *url = (NSURL *)item; - UIImage *image = [[UIImage alloc] initWithContentsOfFile:[url path]]; - if (image != nil) { - UIImage *result = TGScaleImageToPixelSize(image, TGFitSize(image.size, maxSize)); - NSData *resultData = UIImageJPEGRepresentation(result, 0.52f); - if (resultData != nil) { - [subscriber putNext:@{@"scaledImageData": resultData, @"scaledImageDimensions": [NSValue valueWithCGSize:result.size]}]; - [subscriber putCompletion]; - } else { - [subscriber putError:nil]; - } + + CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef) url, NULL); + + CFDictionaryRef options = (__bridge CFDictionaryRef) @{ + (id) kCGImageSourceCreateThumbnailWithTransform : @YES, + (id) kCGImageSourceCreateThumbnailFromImageAlways : @YES, + (id) kCGImageSourceThumbnailMaxPixelSize : @(maxSize.width) + }; + + CGImageRef image = CGImageSourceCreateThumbnailAtIndex(src, 0, options); + CFRelease(src); + + if (image == nil) { + [subscriber putError:nil]; + return; + } + + NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"img%d", (int)arc4random()]]; + CFURLRef tempUrl = (__bridge CFURLRef)[NSURL fileURLWithPath:tempPath]; + CGImageDestinationRef destination = CGImageDestinationCreateWithURL(tempUrl, kUTTypeJPEG, 1, NULL); + NSDictionary *properties = @{ (__bridge NSString *)kCGImageDestinationLossyCompressionQuality: @(0.52)}; + + CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)properties); + CGImageDestinationAddImage(destination, image, nil); + + if (!CGImageDestinationFinalize(destination)) { + CFRelease(destination); + + [subscriber putError:nil]; + return; + } + + CFRelease(destination); + NSData *resultData = [[NSData alloc] initWithContentsOfFile:tempPath options:NSDataReadingMappedIfSafe error:nil]; + if (resultData != nil) { + [subscriber putNext:@{@"scaledImageData": resultData, @"scaledImageDimensions": [NSValue valueWithCGSize:CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image))]}]; + [subscriber putCompletion]; } else { [subscriber putError:nil]; } diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index 413d6da510..4f338d5622 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -190,6 +190,10 @@ public final class CallController: ViewController { c.presentationArguments = a window.present(c, on: .root, blockInteraction: false, completion: {}) } + }, push: { [weak self] c in + if let strongSelf = self { + strongSelf.push(c) + } }) strongSelf.present(controller, in: .window(.root)) }) diff --git a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift index 59d3dd954f..d0ba42de1d 100644 --- a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift +++ b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift @@ -275,6 +275,7 @@ public func callFeedbackController(sharedContext: SharedAccountContext, account: let controller = ItemListController(sharedContext: sharedContext, state: signal) + controller.navigationPresentation = .modal presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window(.root)) } diff --git a/submodules/TelegramCallsUI/Sources/CallRatingController.swift b/submodules/TelegramCallsUI/Sources/CallRatingController.swift index 6568afccea..2bbe0acae1 100644 --- a/submodules/TelegramCallsUI/Sources/CallRatingController.swift +++ b/submodules/TelegramCallsUI/Sources/CallRatingController.swift @@ -266,7 +266,7 @@ func rateCallAndSendLogs(account: Account, callId: CallId, starsCount: Int, comm } } -public func callRatingController(sharedContext: SharedAccountContext, account: Account, callId: CallId, userInitiated: Bool, present: @escaping (ViewController, Any) -> Void) -> AlertController { +public func callRatingController(sharedContext: SharedAccountContext, account: Account, callId: CallId, userInitiated: Bool, present: @escaping (ViewController, Any) -> Void, push: @escaping (ViewController) -> Void) -> AlertController { let presentationData = sharedContext.currentPresentationData.with { $0 } let theme = presentationData.theme let strings = presentationData.strings @@ -282,8 +282,7 @@ public func callRatingController(sharedContext: SharedAccountContext, account: A }, apply: { rating in dismissImpl?(true) if rating < 4 { - let controller = callFeedbackController(sharedContext: sharedContext, account: account, callId: callId, rating: rating, userInitiated: userInitiated) - present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + push(callFeedbackController(sharedContext: sharedContext, account: account, callId: callId, rating: rating, userInitiated: userInitiated)) } else { let _ = rateCallAndSendLogs(account: account, callId: callId, starsCount: rating, comment: "", userInitiated: userInitiated, includeLogs: false).start() } diff --git a/submodules/TelegramCore/TelegramCore/CollectCacheUsageStats.swift b/submodules/TelegramCore/TelegramCore/CollectCacheUsageStats.swift index 8811e63a17..5d0caf84b8 100644 --- a/submodules/TelegramCore/TelegramCore/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/TelegramCore/CollectCacheUsageStats.swift @@ -55,10 +55,17 @@ private final class CacheUsageStatsState { var mediaResourceIds: [MediaId: [MediaResourceId]] = [:] var allResourceIds = Set() var lowerBound: MessageIndex? + var upperBound: MessageIndex? } -public func collectCacheUsageStats(account: Account, additionalCachePaths: [String], logFilesPath: String) -> Signal { - let state = Atomic(value: CacheUsageStatsState()) +public func collectCacheUsageStats(account: Account, peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal { + var initialState = CacheUsageStatsState() + if let peerId = peerId { + initialState.lowerBound = MessageIndex.lowerBound(peerId: peerId) + initialState.upperBound = MessageIndex.upperBound(peerId: peerId) + } + + let state = Atomic(value: initialState) let excludeResourceIds = account.postbox.transaction { transaction -> Set in var result = Set() @@ -72,7 +79,7 @@ public func collectCacheUsageStats(account: Account, additionalCachePaths: [Stri return excludeResourceIds |> mapToSignal { excludeResourceIds -> Signal in let fetch = account.postbox.transaction { transaction -> ([PeerId : Set], [MediaId : Media], MessageIndex?) in - return transaction.enumerateMedia(lowerBound: state.with { $0.lowerBound }, limit: 1000) + return transaction.enumerateMedia(lowerBound: state.with { $0.lowerBound }, upperBound: state.with { $0.upperBound }, limit: 1000) } |> mapError { _ -> CollectCacheUsageStatsError in preconditionFailure() } @@ -169,6 +176,27 @@ public func collectCacheUsageStats(account: Account, additionalCachePaths: [Stri } } if updatedLowerBound == nil { + if peerId != nil { + let (finalMedia, finalMediaResourceIds, allResourceIds) = state.with { state -> ([PeerId: [PeerCacheUsageCategory: [MediaId: Int64]]], [MediaId: [MediaResourceId]], Set) in + return (state.media, state.mediaResourceIds, state.allResourceIds) + } + return account.postbox.transaction { transaction -> CacheUsageStats in + var peers: [PeerId: Peer] = [:] + for peerId in finalMedia.keys { + if let peer = transaction.getPeer(peerId) { + peers[peer.id] = peer + if let associatedPeerId = peer.associatedPeerId, let associatedPeer = transaction.getPeer(associatedPeerId) { + peers[associatedPeer.id] = associatedPeer + } + } + } + return CacheUsageStats(media: finalMedia, mediaResourceIds: finalMediaResourceIds, peers: peers, otherSize: 0, otherPaths: [], cacheSize: 0, tempPaths: [], tempSize: 0, immutableSize: 0) + } |> mapError { _ -> CollectCacheUsageStatsError in preconditionFailure() } + |> mapToSignal { stats -> Signal in + return .fail(.done(stats)) + } + } + let (finalMedia, finalMediaResourceIds, allResourceIds) = state.with { state -> ([PeerId: [PeerCacheUsageCategory: [MediaId: Int64]]], [MediaId: [MediaResourceId]], Set) in return (state.media, state.mediaResourceIds, state.allResourceIds) } @@ -205,7 +233,7 @@ public func collectCacheUsageStats(account: Account, additionalCachePaths: [Stri } } } - if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logFilesPath), includingPropertiesForKeys: [URLResourceKey.fileSizeKey], options: []) { + if let logFilesPath = logFilesPath, let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logFilesPath), includingPropertiesForKeys: [URLResourceKey.fileSizeKey], options: []) { for url in files { if let fileSize = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize { immutableSize += Int64(fileSize) diff --git a/submodules/TelegramCore/TelegramCore/MessageUtils.swift b/submodules/TelegramCore/TelegramCore/MessageUtils.swift index 57adb0fba2..b53da14414 100644 --- a/submodules/TelegramCore/TelegramCore/MessageUtils.swift +++ b/submodules/TelegramCore/TelegramCore/MessageUtils.swift @@ -195,7 +195,7 @@ public extension Message { func effectivelyFailed(timestamp: Int32) -> Bool { if self.flags.contains(.Failed) { return true - } else if self.id.namespace == Namespaces.Message.ScheduledCloud { + } else if self.id.namespace == Namespaces.Message.ScheduledCloud && self.timestamp != 0x7FFFFFFE { return timestamp > self.timestamp + 60 } else { return false diff --git a/submodules/TelegramCore/TelegramCore/Network.swift b/submodules/TelegramCore/TelegramCore/Network.swift index 900e540888..6c36eb080b 100644 --- a/submodules/TelegramCore/TelegramCore/Network.swift +++ b/submodules/TelegramCore/TelegramCore/Network.swift @@ -490,7 +490,7 @@ func initializedNetwork(arguments: NetworkInitializationArguments, supplementary context.keychain = keychain var wrappedAdditionalSource: MTSignal? #if os(iOS) - if #available(iOS 10.0, *) { + if #available(iOS 10.0, *), !supplementary { var cloudDataContextValue: CloudDataContext? if let value = cloudDataContext.with({ $0 }) { cloudDataContextValue = value diff --git a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift index b8e7e2853c..6964197836 100644 --- a/submodules/TelegramPermissionsUI/Sources/PermissionController.swift +++ b/submodules/TelegramPermissionsUI/Sources/PermissionController.swift @@ -9,7 +9,7 @@ import TelegramPresentationData import DeviceAccess import AccountContext -public final class PermissionController : ViewController { +public final class PermissionController: ViewController { private let context: AccountContext private let splitTest: PermissionUISplitTest? private var state: PermissionControllerContent? @@ -238,11 +238,4 @@ public final class PermissionController : ViewController { @objc private func nextPressed() { self.skip?() } - - override public func dismiss(completion: (() -> Void)? = nil) { - self.controllerNode.animateOut(completion: { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - completion?() - }) - } } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 57bd814166..887c0d0c6e 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -85,7 +85,11 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta separatorColor: UIColor(rgb: 0x3d3d40), badgeBackgroundColor: badgeFillColor, badgeStrokeColor: UIColor(rgb: 0x1c1c1d), - badgeTextColor: badgeTextColor + badgeTextColor: badgeTextColor, + segmentedBackgroundColor: UIColor(rgb: 0x3a3b3d), + segmentedForegroundColor: UIColor(rgb: 0x6f7075), + segmentedTextColor: UIColor(rgb: 0xffffff), + segmentedDividerColor: UIColor(rgb: 0x505155) ) let navigationSearchBar = PresentationThemeNavigationSearchBar( diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index 21298a6eab..913f21f507 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -61,7 +61,11 @@ private func makeDarkPresentationTheme(accentColor: UIColor, baseColor: Presenta separatorColor: mainSeparatorColor, badgeBackgroundColor: UIColor(rgb: 0xef5b5b), badgeStrokeColor: UIColor(rgb: 0xef5b5b), - badgeTextColor: UIColor(rgb: 0xffffff) + badgeTextColor: UIColor(rgb: 0xffffff), + segmentedBackgroundColor: mainInputColor, + segmentedForegroundColor: mainBackgroundColor, + segmentedTextColor: UIColor(rgb: 0xffffff), + segmentedDividerColor: mainSecondaryTextColor.withAlphaComponent(0.5) ) let navigationSearchBar = PresentationThemeNavigationSearchBar( diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 8c04bcd836..7343029175 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -69,7 +69,11 @@ private func makeDefaultDayPresentationTheme(accentColor: UIColor, serviceBackgr separatorColor: UIColor(rgb: 0xb1b1b1), badgeBackgroundColor: UIColor(rgb: 0xff3b30), badgeStrokeColor: UIColor(rgb: 0xff3b30), - badgeTextColor: .white + badgeTextColor: .white, + segmentedBackgroundColor: UIColor(rgb: 0xe9e9e9), + segmentedForegroundColor: UIColor(rgb: 0xf7f7f7), + segmentedTextColor: UIColor(rgb: 0x000000), + segmentedDividerColor: UIColor(rgb: 0xd6d6dc) ) let navigationSearchBar = PresentationThemeNavigationSearchBar( diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index d5030c9cbb..0f27252b95 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -106,8 +106,12 @@ public final class PresentationThemeRootNavigationBar { public let badgeBackgroundColor: UIColor public let badgeStrokeColor: UIColor public let badgeTextColor: UIColor + public let segmentedBackgroundColor: UIColor + public let segmentedForegroundColor: UIColor + public let segmentedTextColor: UIColor + public let segmentedDividerColor: UIColor - public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { + public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor, segmentedBackgroundColor: UIColor, segmentedForegroundColor: UIColor, segmentedTextColor: UIColor, segmentedDividerColor: UIColor) { self.buttonColor = buttonColor self.disabledButtonColor = disabledButtonColor self.primaryTextColor = primaryTextColor @@ -119,6 +123,10 @@ public final class PresentationThemeRootNavigationBar { self.badgeBackgroundColor = badgeBackgroundColor self.badgeStrokeColor = badgeStrokeColor self.badgeTextColor = badgeTextColor + self.segmentedBackgroundColor = segmentedBackgroundColor + self.segmentedForegroundColor = segmentedForegroundColor + self.segmentedTextColor = segmentedTextColor + self.segmentedDividerColor = segmentedDividerColor } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift index 84fa353262..b76fe2e399 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeCodable.swift @@ -339,6 +339,10 @@ extension PresentationThemeRootNavigationBar: Codable { case badgeFill case badgeStroke case badgeText + case segmentedBg + case segmentedFg + case segmentedText + case segmentedDivider } public convenience init(from decoder: Decoder) throws { @@ -353,7 +357,11 @@ extension PresentationThemeRootNavigationBar: Codable { separatorColor: try decodeColor(values, .separator), badgeBackgroundColor: try decodeColor(values, .badgeFill), badgeStrokeColor: try decodeColor(values, .badgeStroke), - badgeTextColor: try decodeColor(values, .badgeText)) + badgeTextColor: try decodeColor(values, .badgeText), + segmentedBackgroundColor: try decodeColor(values, .segmentedBg), + segmentedForegroundColor: try decodeColor(values, .segmentedFg), + segmentedTextColor: try decodeColor(values, .segmentedText), + segmentedDividerColor: try decodeColor(values, .segmentedDivider)) } public func encode(to encoder: Encoder) throws { @@ -369,6 +377,10 @@ extension PresentationThemeRootNavigationBar: Codable { try encodeColor(&values, self.badgeBackgroundColor, .badgeFill) try encodeColor(&values, self.badgeStrokeColor, .badgeStroke) try encodeColor(&values, self.badgeTextColor, .badgeText) + try encodeColor(&values, self.segmentedBackgroundColor, .segmentedBg) + try encodeColor(&values, self.segmentedForegroundColor, .segmentedFg) + try encodeColor(&values, self.segmentedTextColor, .segmentedText) + try encodeColor(&values, self.segmentedDividerColor, .segmentedDivider) } } diff --git a/submodules/TelegramUI/TelegramUI/ApplicationShortcutItem.swift b/submodules/TelegramUI/TelegramUI/ApplicationShortcutItem.swift index 43841ee9b3..7746184822 100644 --- a/submodules/TelegramUI/TelegramUI/ApplicationShortcutItem.swift +++ b/submodules/TelegramUI/TelegramUI/ApplicationShortcutItem.swift @@ -24,7 +24,7 @@ extension ApplicationShortcutItem { case .compose: icon = UIApplicationShortcutIcon(type: .compose) case .camera: - icon = UIApplicationShortcutIcon(type: .capturePhoto) + icon = UIApplicationShortcutIcon(templateImageName: "Shortcuts/Camera") case .savedMessages: icon = UIApplicationShortcutIcon(templateImageName: "Shortcuts/SavedMessages") } diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 5ac0cfa5ba..7584fe6fed 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -52,6 +52,7 @@ import WalletUI import WalletUrl import LocalizedPeerData import PhoneNumberFormat +import SettingsUI public enum ChatControllerPeekActions { case standard @@ -266,6 +267,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var raiseToListen: RaiseToListenManager? private var voicePlaylistDidEndTimestamp: Double = 0.0 + private weak var searchResultsTooltipController: TooltipController? private weak var messageTooltipController: TooltipController? private weak var videoUnmuteTooltipController: TooltipController? private weak var silentPostTooltipController: TooltipController? @@ -1149,7 +1151,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case let .url(url): var cleanUrl = url var canAddToReadingList = true - let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 + var canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 let mailtoString = "mailto:" let telString = "tel:" var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen @@ -1162,6 +1164,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) cleanUrl = phoneNumber! openText = strongSelf.presentationData.strings.UserInfo_PhoneCall + canOpenIn = false } else if canOpenIn { openText = strongSelf.presentationData.strings.Conversation_FileOpenIn } @@ -1470,6 +1473,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.present(c, in: .window(.root), with: a) } + }, push: { [weak self] c in + if let strongSelf = self { + strongSelf.push(c) + } }) strongSelf.chatDisplayNode.dismissInput() strongSelf.present(controller, in: .window(.root)) @@ -1518,22 +1525,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.context.sharedContext.applicationBindings.openAppStorePage() } - }, displayMessageTooltip: { [weak self] messageId, text, sourceNode, sourceFrame in + }, displayMessageTooltip: { [weak self] messageId, text, node, nodeRect in if let strongSelf = self { - if let sourceNode = sourceNode { + if let node = node { strongSelf.messageTooltipController?.dismiss() let tooltipController = TooltipController(content: .text(text), dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) strongSelf.messageTooltipController = tooltipController - tooltipController.dismissed = { [weak tooltipController] in + tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.messageTooltipController === tooltipController { strongSelf.messageTooltipController = nil } } strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { if let strongSelf = self { - var rect = sourceNode.view.convert(sourceNode.view.bounds, to: strongSelf.chatDisplayNode.view) - if let sourceFrame = sourceFrame { - rect = CGRect(origin: rect.origin.offsetBy(dx: sourceFrame.minX, dy: sourceFrame.minY - sourceNode.bounds.minY), size: sourceFrame.size) + var rect = node.view.convert(node.view.bounds, to: strongSelf.chatDisplayNode.view) + if let nodeRect = nodeRect { + rect = CGRect(origin: rect.origin.offsetBy(dx: nodeRect.minX, dy: nodeRect.minY - node.bounds.minY), size: nodeRect.size) } return (strongSelf.chatDisplayNode, rect) } @@ -1685,6 +1692,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.present(MessageReactionListController(context: strongSelf.context, messageId: message.id, initialReactions: initialReactions), in: .window(.root)) } }) + }, displaySwipeToReplyHint: { [weak self] in + if let strongSelf = self { + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .swipeToReply(title: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintTitle, text: strongSelf.presentationData.strings.Conversation_SwipeToReplyHintText), elevatedLayout: true, action: { _ in }), in: .window(.root)) + } }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) @@ -2997,6 +3008,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self, let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { strongSelf.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), present: { c, a in self?.present(c, in: .window(.root), with: a) + }, push: { c in + self?.push(c) }, completion: { _ in }), in: .window(.root)) } }, reportMessages: { [weak self] messages, contextController in @@ -3562,7 +3575,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let tooltipController = TooltipController(content: .text(banDescription)) strongSelf.mediaRestrictedTooltipController = tooltipController strongSelf.mediaRestrictedTooltipControllerMode = isStickers - tooltipController.dismissed = { [weak tooltipController] in + tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRestrictedTooltipController === tooltipController { strongSelf.mediaRestrictedTooltipController = nil } @@ -3597,7 +3610,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.videoUnmuteTooltipController?.dismiss() let tooltipController = TooltipController(content: .iconAndText(icon, strongSelf.presentationInterfaceState.strings.Conversation_PressVolumeButtonForSound), timeout: 3.5, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) strongSelf.videoUnmuteTooltipController = tooltipController - tooltipController.dismissed = { [weak tooltipController] in + tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.videoUnmuteTooltipController === tooltipController { strongSelf.videoUnmuteTooltipController = nil ApplicationSpecificNotice.setVolumeButtonToUnmute(accountManager: strongSelf.context.sharedContext.accountManager) @@ -3852,7 +3865,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if let rect = rect { let tooltipController = TooltipController(content: .text(text)) strongSelf.silentPostTooltipController = tooltipController - tooltipController.dismissed = { [weak tooltipController] in + tooltipController.dismissed = { [weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.silentPostTooltipController === tooltipController { strongSelf.silentPostTooltipController = nil } @@ -4078,6 +4091,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { strongSelf.openScheduledMessages() } + }, displaySearchResultsTooltip: { [weak self] node, nodeRect in + if let strongSelf = self { + strongSelf.searchResultsTooltipController?.dismiss() + let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.ChatSearch_ResultsTooltip), dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true) + strongSelf.searchResultsTooltipController = tooltipController + tooltipController.dismissed = { [weak tooltipController] _ in + if let strongSelf = self, let tooltipController = tooltipController, strongSelf.searchResultsTooltipController === tooltipController { + strongSelf.searchResultsTooltipController = nil + } + } + strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { + if let strongSelf = self { + var rect = node.view.convert(node.view.bounds, to: strongSelf.chatDisplayNode.view) + rect = CGRect(origin: rect.origin.offsetBy(dx: nodeRect.minX, dy: nodeRect.minY - node.bounds.minY), size: nodeRect.size) + return (strongSelf.chatDisplayNode, rect) + } + return nil + })) + } }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) switch self.chatLocation { @@ -5052,19 +5084,213 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch self.chatLocationInfoData { case let .peer(peerView): self.navigationActionDisposable.set((peerView.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios") == nil && !strongSelf.presentationInterfaceState.isNotAccessible { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { - strongSelf.effectiveNavigationController?.pushViewController(infoController) - } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios") == nil && !strongSelf.presentationInterfaceState.isNotAccessible { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + strongSelf.effectiveNavigationController?.pushViewController(infoController) } + } })) } case .search: self.interfaceInteraction?.beginMessageSearch(.everything, "") case .dismiss: self.dismiss() + case .clearCache: + let clearDisposable = MetaDisposable() + + switch self.chatLocationInfoData { + case let .peer(peerView): + self.navigationActionDisposable.set((peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + guard let strongSelf = self, let peer = peerView.peers[peerView.peerId] else { + return + } + let peerId = peer.id + + let cacheUsageStats = (collectCacheUsageStats(account: strongSelf.context.account, peerId: peer.id) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self, case let .result(stats) = result, var categories = stats.media[peer.id] else { + return + } + let presentationData = strongSelf.presentationData + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + + var sizeIndex: [PeerCacheUsageCategory: (Bool, Int64)] = [:] + + var itemIndex = 0 + + let updateTotalSize: () -> Void = { [weak controller] in + controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in + let title: String + let filteredSize = sizeIndex.values.reduce(0, { $0 + ($1.0 ? $1.1 : 0) }) + + if filteredSize == 0 { + title = presentationData.strings.Cache_ClearNone + } else { + title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0 + } + + if let item = item as? ActionSheetButtonItem { + return ActionSheetButtonItem(title: title, color: filteredSize != 0 ? .accent : .disabled, enabled: filteredSize != 0, action: item.action) + } + return item + }) + } + + let toggleCheck: (PeerCacheUsageCategory, Int) -> Void = { [weak controller] category, itemIndex in + if let (value, size) = sizeIndex[category] { + sizeIndex[category] = (!value, size) + } + controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in + if let item = item as? ActionSheetCheckboxItem { + return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) + } + return item + }) + updateTotalSize() + } + var items: [ActionSheetItem] = [] + + items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: peer, chatPeer: peer, action: .clearCache, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder)) + + let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file] + + var totalSize: Int64 = 0 + + func stringForCategory(strings: PresentationStrings, category: PeerCacheUsageCategory) -> String { + switch category { + case .image: + return strings.Cache_Photos + case .video: + return strings.Cache_Videos + case .audio: + return strings.Cache_Music + case .file: + return strings.Cache_Files + } + } + + for categoryId in validCategories { + if let media = categories[categoryId] { + var categorySize: Int64 = 0 + for (_, size) in media { + categorySize += size + } + sizeIndex[categoryId] = (true, categorySize) + totalSize += categorySize + if categorySize > 1024 { + let index = itemIndex + items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), value: true, action: { value in + toggleCheck(categoryId, index) + })) + itemIndex += 1 + } + } + } + + if items.isEmpty { + strongSelf.presentClearCacheSuggestion() + } else { + items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))").0, action: { + let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) + var clearMediaIds = Set() + + var media = stats.media + if var categories = media[peerId] { + for category in clearCategories { + if let contents = categories[category] { + for (mediaId, _) in contents { + clearMediaIds.insert(mediaId) + } + } + categories.removeValue(forKey: category) + } + + media[peerId] = categories + } +// if let additionalPeerId = additionalPeerId { +// if var categories = media[additionalPeerId] { +// for category in clearCategories { +// if let contents = categories[category] { +// for (mediaId, _) in contents { +// clearMediaIds.insert(mediaId) +// } +// } +// categories.removeValue(forKey: category) +// } +// +// media[additionalPeerId] = categories +// } +// } + + var clearResourceIds = Set() + for id in clearMediaIds { + if let ids = stats.mediaResourceIds[id] { + for resourceId in ids { + clearResourceIds.insert(WrappedMediaResourceId(resourceId)) + } + } + } + + var signal = clearCachedMediaResources(account: strongSelf.context.account, mediaResourceIds: clearResourceIds) + + let resultStats = CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: stats.otherSize, otherPaths: stats.otherPaths, cacheSize: stats.cacheSize, tempPaths: stats.tempPaths, tempSize: stats.tempSize, immutableSize: stats.immutableSize) + + var cancelImpl: (() -> Void)? + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + signal = signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + clearDisposable.set(nil) + } + clearDisposable.set((signal + |> deliverOnMainQueue).start(completed: { [weak self] in + if let strongSelf = self, let layout = strongSelf.validLayout { + let deviceName = UIDevice.current.userInterfaceIdiom == .pad ? "iPad" : "iPhone" + strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", deviceName).0), elevatedLayout: true, action: { _ in }), in: .window(.root)) + } + })) + + dismissAction() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + })) + + controller.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.chatDisplayNode.dismissInput() + strongSelf.present(controller, in: .window(.root)) + } + }) + })) + } } } @@ -7506,6 +7732,35 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + private func presentClearCacheSuggestion() { + guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + + let actionSheet = ActionSheetController(presentationTheme: self.presentationData.theme) + var items: [ActionSheetItem] = [] + + items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: peer, chatPeer: peer, action: .clearCacheSuggestion, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder)) + + items.append(ActionSheetButtonItem(title: self.presentationData.strings.ClearCache_FreeSpace, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + let controller = storageUsageController(context: strongSelf.context, isModal: true) + strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + }) + ])]) + self.chatDisplayNode.dismissInput() + self.present(actionSheet, in: .window(.root)) + } + @available(iOSApplicationExtension 11.0, iOS 11.0, *) public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String]) @@ -7569,7 +7824,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else if let rect = rect { let tooltipController = TooltipController(content: .text(text)) self.mediaRecordingModeTooltipController = tooltipController - tooltipController.dismissed = { [weak self, weak tooltipController] in + tooltipController.dismissed = { [weak self, weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.mediaRecordingModeTooltipController === tooltipController { strongSelf.mediaRecordingModeTooltipController = nil } @@ -7584,6 +7839,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } private func dismissAllTooltips() { + self.searchResultsTooltipController?.dismiss() self.messageTooltipController?.dismiss() self.videoUnmuteTooltipController?.dismiss() self.silentPostTooltipController?.dismiss() diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift index ffc9670b72..a711a465b9 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift @@ -100,6 +100,7 @@ public final class ChatControllerInteraction { let performTextSelectionAction: (UInt32, String, TextSelectionAction) -> Void let updateMessageReaction: (MessageId, String) -> Void let openMessageReactions: (MessageId) -> Void + let displaySwipeToReplyHint: () -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -114,7 +115,7 @@ public final class ChatControllerInteraction { var searchTextHighightState: (String, [MessageIndex])? var seenOneTimeAnimatedMedia = Set() - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String) -> Void, openMessageReactions: @escaping (MessageId) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -166,7 +167,8 @@ public final class ChatControllerInteraction { self.performTextSelectionAction = performTextSelectionAction self.updateMessageReaction = updateMessageReaction self.openMessageReactions = openMessageReactions - + self.displaySwipeToReplyHint = displaySwipeToReplyHint + self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures @@ -202,6 +204,7 @@ public final class ChatControllerInteraction { }, performTextSelectionAction: { _, _, _ in }, updateMessageReaction: { _, _ in }, openMessageReactions: { _ in + }, displaySwipeToReplyHint: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift index 148bde606c..ff5e249b2d 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift @@ -1533,6 +1533,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }) } self.searchNavigationNode?.deactivate() + + self.view.window?.endEditing(true) } private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { @@ -2122,6 +2124,19 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } + + var forwardingToSameChat = false + if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds { + for messageId in forwardMessageIds { + if messageId.peerId == id { + forwardingToSameChat = true + } + } + } + if !messages.isEmpty && forwardingToSameChat { + //self.controllerInteraction.displaySwipeToReplyHint() + } + if !messages.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { self.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift index 5c8fb99623..bf3eafbf57 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -9,6 +9,7 @@ import AccountContext enum ChatNavigationButtonAction { case openChatInfo case clearHistory + case clearCache case cancelMessageSelection case search case dismiss @@ -45,6 +46,9 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha if canClear { return ChatNavigationButton(action: .clearHistory, buttonItem: UIBarButtonItem(title: title, style: .plain, target: target, action: selector)) + } else { + title = strings.Conversation_ClearCache + return ChatNavigationButton(action: .clearCache, buttonItem: UIBarButtonItem(title: title, style: .plain, target: target, action: selector)) } } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageDateHeader.swift b/submodules/TelegramUI/TelegramUI/ChatMessageDateHeader.swift index d00aad8232..27ed5cc11b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageDateHeader.swift @@ -32,7 +32,9 @@ final class ChatMessageDateHeader: ListViewItemHeader { self.presentationData = presentationData self.context = context self.action = action - if timestamp == Int32.max { + if timestamp == 0x7FFFFFFE { + self.roundedTimestamp = 0x7FFFFFFE + } else if timestamp == Int32.max { self.roundedTimestamp = timestamp / (granularity) * (granularity) } else { self.roundedTimestamp = ((timestamp + timezoneOffset) / (granularity)) * (granularity) @@ -151,7 +153,9 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { } if scheduled { - if timeinfo.tm_year == timeinfoNow.tm_year && timeinfo.tm_yday == timeinfoNow.tm_yday { + if localTimestamp == 0x7FFFFFFE { + text = presentationData.strings.ScheduledMessages_ScheduledOnline + } else if timeinfo.tm_year == timeinfoNow.tm_year && timeinfo.tm_yday == timeinfoNow.tm_yday { text = presentationData.strings.ScheduledMessages_ScheduledToday } else { text = presentationData.strings.ScheduledMessages_ScheduledDate(text).0 diff --git a/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift index d0d9942b2e..dcd5621f02 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessagePollBubbleContentNode.swift @@ -312,7 +312,7 @@ private struct ChatMessagePollOptionResult: Equatable { } private final class ChatMessagePollOptionNode: ASDisplayNode { - private let highlightedBackgroundNode: ASImageNode + private let highlightedBackgroundNode: ASDisplayNode private var radioNode: ChatMessagePollOptionRadioNode? private let percentageNode: ASDisplayNode private var percentageImage: UIImage? @@ -326,10 +326,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { var pressed: (() -> Void)? override init() { - self.highlightedBackgroundNode = ASImageNode() - self.highlightedBackgroundNode.displayWithoutProcessing = true - self.highlightedBackgroundNode.displaysAsynchronously = false - self.highlightedBackgroundNode.isLayerBacked = true + self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.alpha = 0.0 self.highlightedBackgroundNode.isUserInteractionEnabled = false @@ -344,8 +341,6 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { self.percentageNode = ASDisplayNode() self.percentageNode.alpha = 0.0 self.percentageNode.isLayerBacked = true - //self.percentageNode.displaysAsynchronously = false - //self.percentageNode.displayWithoutProcessing = true super.init() @@ -407,7 +402,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { let previousResult = node.currentResult node.currentResult = optionResult - node.highlightedBackgroundNode.backgroundColor = (incoming ? presentationData.theme.theme.chat.message.incoming.accentTextColor : presentationData.theme.theme.chat.message.outgoing.accentTextColor).withAlphaComponent(0.15) + node.highlightedBackgroundNode.backgroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.highlight : presentationData.theme.theme.chat.message.outgoing.polls.highlight node.buttonNode.accessibilityLabel = option.text diff --git a/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift b/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift index ee7ed6b3e3..47c6496061 100644 --- a/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/submodules/TelegramUI/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -113,9 +113,10 @@ final class ChatPanelInterfaceInteraction { let displaySlowmodeTooltip: (ASDisplayNode, CGRect) -> Void let displaySendMessageOptions: () -> Void let openScheduledMessages: () -> Void + let displaySearchResultsTooltip: (ASDisplayNode, CGRect) -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, setupEditMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message], ContextController?) -> Void, deleteMessages: @escaping ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, openSearchResults: @escaping () -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, displaySendMessageOptions: @escaping () -> Void, openScheduledMessages: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, setupEditMessage: @escaping (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void, beginMessageSelection: @escaping ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void, deleteSelectedMessages: @escaping () -> Void, reportSelectedMessages: @escaping () -> Void, reportMessages: @escaping ([Message], ContextController?) -> Void, deleteMessages: @escaping ([Message], ContextController?, @escaping (ContextMenuActionResult) -> Void) -> Void, forwardSelectedMessages: @escaping () -> Void, forwardCurrentForwardMessages: @escaping () -> Void, forwardMessages: @escaping ([Message]) -> Void, shareSelectedMessages: @escaping () -> Void, updateTextInputStateAndMode: @escaping ((ChatTextInputState, ChatInputMode) -> (ChatTextInputState, ChatInputMode)) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, openStickers: @escaping () -> Void, editMessage: @escaping () -> Void, beginMessageSearch: @escaping (ChatSearchDomain, String) -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, openSearchResults: @escaping () -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, toggleMembersSearch: @escaping (Bool) -> Void, navigateToMessage: @escaping (MessageId) -> Void, navigateToChat: @escaping (PeerId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect) -> Bool, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginMediaRecording: @escaping (Bool) -> Void, finishMediaRecording: @escaping (ChatFinishMediaRecordingAction) -> Void, stopMediaRecording: @escaping () -> Void, lockMediaRecording: @escaping () -> Void, deleteRecordedMedia: @escaping () -> Void, sendRecordedMedia: @escaping () -> Void, displayRestrictedInfo: @escaping (ChatPanelRestrictionInfoSubject, ChatPanelRestrictionInfoDisplayType) -> Void, displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, shareAccountContact: @escaping () -> Void, reportPeer: @escaping () -> Void, presentPeerContact: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, beginCall: @escaping () -> Void, toggleMessageStickerStarred: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigateFeed: @escaping () -> Void, openGrouping: @escaping () -> Void, toggleSilentPost: @escaping () -> Void, requestUnvoteInMessage: @escaping (MessageId) -> Void, requestStopPollInMessage: @escaping (MessageId) -> Void, updateInputLanguage: @escaping ((String?) -> String?) -> Void, unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, displaySendMessageOptions: @escaping () -> Void, openScheduledMessages: @escaping () -> Void, displaySearchResultsTooltip: @escaping (ASDisplayNode, CGRect) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection @@ -182,6 +183,7 @@ final class ChatPanelInterfaceInteraction { self.displaySlowmodeTooltip = displaySlowmodeTooltip self.displaySendMessageOptions = displaySendMessageOptions self.openScheduledMessages = openScheduledMessages + self.displaySearchResultsTooltip = displaySearchResultsTooltip self.statuses = statuses } } diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift index 8d372b0713..472b19bd5a 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsController.swift @@ -114,6 +114,7 @@ final class ChatRecentActionsController: TelegramBaseController { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { }, openScheduledMessages: { + }, displaySearchResultsTooltip: { _, _ in }, statuses: nil) self.navigationItem.titleView = self.titleView diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift index 55fd66e5c7..e09deb9a21 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift @@ -416,6 +416,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, performTextSelectionAction: { _, _, _ in }, updateMessageReaction: { _, _ in }, openMessageReactions: { _ in + }, displaySwipeToReplyHint: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, diff --git a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeController.swift b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeController.swift index a64cbdcb42..949d343aa0 100644 --- a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeController.swift @@ -64,7 +64,7 @@ final class ChatScheduleTimeController: ViewController { override public func loadDisplayNode() { self.displayNode = ChatScheduleTimeControllerNode(context: self.context, mode: self.mode, currentTime: self.currentTime, minimalTime: self.minimalTime, dismissByTapOutside: self.dismissByTapOutside) self.controllerNode.completion = { [weak self] time in - self?.completion(time + 5) + self?.completion(time != 0x7FFFFFFE ? time + 5 : time) self?.dismiss() } self.controllerNode.dismiss = { [weak self] in diff --git a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift index a6959444b5..c72b17af5f 100644 --- a/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatScheduleTimeControllerNode.swift @@ -26,6 +26,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel private let titleNode: ASTextNode private let cancelButton: HighlightableButtonNode private let doneButton: SolidRoundedButtonNode + private let onlineButton: HighlightableButtonNode private var pickerView: UIDatePicker? private let dateFormatter: DateFormatter @@ -76,6 +77,9 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false) + self.onlineButton = HighlightableButtonNode() + self.onlineButton.setTitle(self.presentationData.strings.Conversation_ScheduleMessage_SendWhenOnline, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) + self.dateFormatter = DateFormatter() self.dateFormatter.timeStyle = .none self.dateFormatter.dateStyle = .short @@ -98,6 +102,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel self.contentContainerNode.addSubnode(self.titleNode) self.contentContainerNode.addSubnode(self.cancelButton) self.contentContainerNode.addSubnode(self.doneButton) + //self.contentContainerNode.addSubnode(self.onlineButton) self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) self.doneButton.pressed = { [weak self] in @@ -112,6 +117,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel } } } + self.onlineButton.addTarget(self, action: #selector(self.onlineButtonPressed), forControlEvents: .touchUpInside) self.setupPickerView(currentTime: currentTime) self.updateButtonTitle() @@ -131,7 +137,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel pickerView.timeZone = TimeZone.current pickerView.minuteInterval = 1 pickerView.setValue(self.presentationData.theme.actionSheet.primaryTextColor, forKey: "textColor") - contentContainerNode.view.addSubview(pickerView) + self.contentContainerNode.view.addSubview(pickerView) pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged) self.pickerView = pickerView @@ -155,6 +161,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) + self.onlineButton.setTitle(self.presentationData.strings.Conversation_ScheduleMessage_SendWhenOnline, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) } private func updateMinimumDate(currentTime: Int32? = nil) { @@ -234,6 +241,10 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel self.cancel?() } + @objc func onlineButtonPressed() { + self.completion?(0x7FFFFFFE) + } + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if self.dismissByTapOutside, case .ended = recognizer.state { self.cancelButtonPressed() @@ -300,10 +311,12 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel let cleanInsets = layout.insets(options: [.statusBar]) insets.top = max(10.0, insets.top) + var buttonOffset: CGFloat = 0.0 //44.0 + let bottomInset: CGFloat = 10.0 + cleanInsets.bottom let titleHeight: CGFloat = 54.0 - var contentHeight = titleHeight + bottomInset + 52.0 + 17.0 - let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight) + var contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + buttonOffset + let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight - buttonOffset) contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + pickerHeight let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left) @@ -330,7 +343,11 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel let buttonInset: CGFloat = 16.0 let buttonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - buttonHeight - insets.bottom - 10.0, width: contentFrame.width, height: buttonHeight)) + transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - buttonHeight - insets.bottom - 10.0 - buttonOffset, width: contentFrame.width, height: buttonHeight)) + + let onlineSize = self.onlineButton.measure(CGSize(width: width, height: titleHeight)) + let onlineFrame = CGRect(origin: CGPoint(x: ceil((layout.size.width - onlineSize.width) / 2.0), y: contentHeight - 36.0 - insets.bottom), size: onlineSize) + transition.updateFrame(node: self.onlineButton, frame: onlineFrame) self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight)) diff --git a/submodules/TelegramUI/TelegramUI/ChatSearchInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatSearchInputPanelNode.swift index 47974bf129..d66c1d5c96 100644 --- a/submodules/TelegramUI/TelegramUI/ChatSearchInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatSearchInputPanelNode.swift @@ -6,6 +6,7 @@ import TelegramCore import SyncCore import Postbox import SwiftSignalKit +import TelegramNotices import TelegramPresentationData import ActivityIndicator @@ -25,6 +26,8 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { private let activityDisposable = MetaDisposable() private var displayActivity = false + private var needsSearchResultsTooltip = true + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, LayoutMetrics)? override var interfaceInteraction: ChatPanelInterfaceInteraction? { @@ -80,6 +83,26 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { @objc func upPressed() { self.interfaceInteraction?.navigateMessageSearch(.earlier) + + guard self.needsSearchResultsTooltip, let context = self.context else { + return + } + + let _ = (ApplicationSpecificNotice.getChatMessageSearchResultsTip(accountManager: context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] counter in + guard let strongSelf = self else { + return + } + + if counter >= 3 { + strongSelf.needsSearchResultsTooltip = false + } else if arc4random_uniform(4) == 1 { + strongSelf.needsSearchResultsTooltip = false + + let _ = ApplicationSpecificNotice.incrementChatMessageSearchResultsTip(accountManager: context.sharedContext.accountManager).start() + strongSelf.interfaceInteraction?.displaySearchResultsTooltip(strongSelf.resultsButton, strongSelf.resultsButton.bounds) + } + }) } @objc func downPressed() { @@ -96,6 +119,10 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { @objc func resultsPressed() { self.interfaceInteraction?.openSearchResults() + + if let context = self.context { + let _ = ApplicationSpecificNotice.incrementChatMessageSearchResultsTip(accountManager: context.sharedContext.accountManager, count: 4).start() + } } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { diff --git a/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift b/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift index 13dca0bb62..a310fdbf8f 100644 --- a/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatTextInputPanelNode.swift @@ -1576,7 +1576,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in if let inputText = current.inputText.mutableCopy() as? NSMutableAttributedString { inputText.replaceCharacters(in: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count), with: attributedString) - return (ChatTextInputState(inputText: inputText), inputMode) + let updatedRange = current.selectionRange.lowerBound + attributedString.length + return (ChatTextInputState(inputText: inputText, selectionRange: updatedRange ..< updatedRange), inputMode) } else { return (ChatTextInputState(inputText: attributedString), inputMode) } diff --git a/submodules/TelegramUI/TelegramUI/CreateGroupController.swift b/submodules/TelegramUI/TelegramUI/CreateGroupController.swift index e28f14603a..735f694f24 100644 --- a/submodules/TelegramUI/TelegramUI/CreateGroupController.swift +++ b/submodules/TelegramUI/TelegramUI/CreateGroupController.swift @@ -315,6 +315,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] var replaceControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var pushImpl: ((ViewController) -> Void)? var endEditingImpl: (() -> Void)? var clearHighlightImpl: (() -> Void)? @@ -584,7 +585,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] }, sendLiveLocation: { _, _ in }, theme: presentationData.theme, customLocationPicker: true, presentationCompleted: { clearHighlightImpl?() }) - presentControllerImpl?(controller, nil) + pushImpl?(controller) }) let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), context.account.postbox.multiplePeersView(peerIds), .single(nil) |> then(addressPromise.get())) @@ -620,6 +621,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } + pushImpl = { [weak controller] c in + controller?.push(c) + } controller.willDisappear = { _ in endEditingImpl?() } diff --git a/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift b/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift index 7a5bd10655..0381ccf30f 100644 --- a/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift +++ b/submodules/TelegramUI/TelegramUI/OpenChatMessage.swift @@ -19,6 +19,7 @@ import PeerInfoUI import SettingsUI import AlertUI import PresentationDataUtils +import ShareController private enum ChatMessageGalleryControllerData { case url(String) @@ -27,7 +28,7 @@ private enum ChatMessageGalleryControllerData { case map(TelegramMediaMap) case stickerPack(StickerPackReference) case audio(TelegramMediaFile) - case document(TelegramMediaFile) + case document(TelegramMediaFile, Bool) case gallery(GalleryController) case secretGallery(SecretMediaPreviewController) case chatAvatars(AvatarGalleryController, Media) @@ -153,14 +154,10 @@ private func chatMessageGalleryControllerData(context: AccountContext, message: return .gallery(gallery) } } - #if DEBUG + if ext == "mkv" { - let gallery = GalleryController(context: context, source: standalone ? .standaloneMessage(message) : .peerMessagesAtId(message.id), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in - navigationController?.replaceTopController(controller, animated: false, ready: ready) - }, baseNavigationController: navigationController, actionInteraction: actionInteraction) - return .gallery(gallery) + return .document(file, true) } - #endif } if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName ?? "file") { @@ -171,7 +168,7 @@ private func chatMessageGalleryControllerData(context: AccountContext, message: } if !file.isVideo { - return .document(file) + return .document(file, false) } } @@ -272,9 +269,12 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.dismissInput() params.present(controller, nil) return true - case let .document(file): + case let .document(file, immediateShare): let presentationData = params.context.sharedContext.currentPresentationData.with { $0 } - if let rootController = params.navigationController?.view.window?.rootViewController { + if immediateShare { + let controller = ShareController(context: params.context, subject: .media(.standalone(media: file)), immediateExternalShare: true) + params.present(controller, nil) + } else if let rootController = params.navigationController?.view.window?.rootViewController { presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file) } return true diff --git a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift index 6cc0532fe0..0074f5a77d 100644 --- a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift @@ -117,6 +117,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, performTextSelectionAction: { _, _, _ in }, updateMessageReaction: { _, _ in }, openMessageReactions: { _ in + }, displaySwipeToReplyHint: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) diff --git a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift index 7bb46ba00a..ca7a4112ab 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift @@ -405,6 +405,7 @@ public class PeerMediaCollectionController: TelegramBaseController { }, performTextSelectionAction: { _, _, _ in }, updateMessageReaction: { _, _ in }, openMessageReactions: { _ in + }, displaySwipeToReplyHint: { }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, @@ -423,6 +424,8 @@ public class PeerMediaCollectionController: TelegramBaseController { if let strongSelf = self, let messageIds = strongSelf.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { strongSelf.present(peerReportOptionsController(context: strongSelf.context, subject: .messages(Array(messageIds).sorted()), present: { c, a in self?.present(c, in: .window(.root), with: a) + }, push: { c in + self?.push(c) }, completion: { _ in }), in: .window(.root)) } }, reportMessages: { _, _ in @@ -517,6 +520,7 @@ public class PeerMediaCollectionController: TelegramBaseController { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { }, openScheduledMessages: { + }, displaySearchResultsTooltip: { _, _ in }, statuses: nil) self.updateInterfaceState(animated: false, { return $0 }) diff --git a/submodules/TelegramUI/TelegramUI/Resources/Animations/anim_swipereply.json b/submodules/TelegramUI/TelegramUI/Resources/Animations/anim_swipereply.json new file mode 100644 index 0000000000..21899c4022 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/Resources/Animations/anim_swipereply.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":60,"ip":0,"op":120,"w":72,"h":72,"nm":"swipereply","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Oval","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[-100]},{"t":90,"s":[-90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[46,36,0],"to":[-3.972,0,0],"ti":[1.667,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[22.167,36,0],"to":[-1.667,0,0],"ti":[-2.306,0,0]},{"t":75,"s":[36,36,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[12.5,12.5,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[18.333,18.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[8.333,8.333,100]},{"t":75,"s":[26.667,26.667,100]}],"ix":6}},"ao":0,"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Bubble","parent":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":30,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[9,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[100,100,100]},{"t":70,"s":[20,20,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.14,-4.29],[0,0],[4.29,-0.14],[0,0],[1.37,1.09],[0.86,-0.01],[0,0],[-0.01,0.21],[0,0],[0,0],[-4.14,0.13]],"o":[[4.33,0],[0,0],[0,4.33],[0,0],[-1.88,0],[-1.51,1.59],[0,0],[2.79,-1.62],[0,0],[0,0],[0.24,-4.12],[0,0]],"v":[[1.5,-8],[9.5,-0.25],[9.5,0],[1.75,8],[1.5,8],[-3.48,6.26],[-9.3,8],[-9.5,8],[-6.5,4.09],[-6.5,-0.5],[-6.49,-0.46],[1.25,-8]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":70,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Reply","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"s":true,"x":{"a":0,"k":0,"ix":3},"y":{"a":0,"k":0,"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[9.55,0],[0,0],[0.59,0],[0.2,-0.19],[0,0],[-0.41,-0.44],[0,0],[0,0],[0,0],[-0.4,0.43],[0,0.27],[0,0],[-2.41,-4.54],[0,0],[-0.18,0.1],[0.01,0.14]],"o":[[0,0],[0,-0.59],[-0.27,0],[0,0],[-0.43,0.41],[0,0],[0,0],[0,0],[0.43,0.41],[0.19,-0.2],[0,0],[7.09,0],[0,0],[0.09,0.17],[0.12,-0.06],[-0.42,-11.87]],"v":[[-0.215,-5.596],[-0.215,-11.536],[-1.285,-12.606],[-2.015,-12.316],[-14.395,-0.786],[-14.445,0.734],[-14.395,0.784],[-14.395,0.784],[-2.015,12.314],[-0.505,12.264],[-0.215,11.534],[-0.215,5.604],[14.045,12.404],[14.055,12.404],[14.545,12.544],[14.735,12.214]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Oval 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[20]},{"t":35,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[46,36,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.398,0.398,0.593],"y":[1,1,-22.251]},"o":{"x":[0.202,0.202,0.175],"y":[0.15,0.15,10.582]},"t":0,"s":[0,0,100]},{"t":20,"s":[30,30,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"contour","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Oval 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Track","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":50,"s":[20]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[36,36,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[16.667,16.667,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[{"i":[[0,-4.418],[0,0],[4.418,0],[0,4.418],[0,0],[-4.418,0]],"o":[[0,0],[0,4.418],[-4.418,0],[0,0],[0,-4.418],[4.418,0]],"v":[[8,-10],[8,-10],[0,-2],[-8,-10],[-8,-10],[0,-18]],"c":true}]},{"t":60,"s":[{"i":[[0,-4.418],[0,0],[4.418,0],[0,4.418],[0,0],[-4.418,0]],"o":[[0,0],[0,4.418],[-4.418,0],[0,0],[0,-4.418],[4.418,0]],"v":[[8,-6.667],[8,13.333],[0,21.333],[-8,13.333],[-8,-6.667],[0,-14.667]],"c":true}]}],"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/TelegramUI/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/TelegramUI/StringForMessageTimestampStatus.swift index d54a11875b..947ba2db53 100644 --- a/submodules/TelegramUI/TelegramUI/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/TelegramUI/StringForMessageTimestampStatus.swift @@ -20,6 +20,9 @@ func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Message, da timestamp = message.timestamp } var dateText = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat) + if timestamp == 0x7FFFFFFE { + dateText = " " + } var authorTitle: String? if let author = message.author as? TelegramUser { diff --git a/submodules/TelegramUI/TelegramUI/WalletContextImpl.swift b/submodules/TelegramUI/TelegramUI/WalletContextImpl.swift index a334debd0b..a17c8a7f2f 100644 --- a/submodules/TelegramUI/TelegramUI/WalletContextImpl.swift +++ b/submodules/TelegramUI/TelegramUI/WalletContextImpl.swift @@ -141,60 +141,63 @@ final class WalletContextImpl: WalletContext { buttonTextColor: .white, incomingFundsTitleColor: theme.chatList.secretTitleColor, outgoingFundsTitleColor: theme.list.itemDestructiveColor - ), setup: WalletSetupTheme( - buttonFillColor: theme.list.itemCheckColors.fillColor, - buttonForegroundColor: theme.list.itemCheckColors.foregroundColor, - inputBackgroundColor: theme.actionSheet.inputBackgroundColor, - inputPlaceholderColor: theme.actionSheet.inputPlaceholderColor, - inputTextColor: theme.actionSheet.inputTextColor, - inputClearButtonColor: theme.actionSheet.inputClearButtonColor.withAlphaComponent(0.8) - ), - list: WalletListTheme( - itemPrimaryTextColor: theme.list.itemPrimaryTextColor, - itemSecondaryTextColor: theme.list.itemSecondaryTextColor, - itemPlaceholderTextColor: theme.list.itemPlaceholderTextColor, - itemDestructiveColor: theme.list.itemDestructiveColor, - itemAccentColor: theme.list.itemAccentColor, - itemDisabledTextColor: theme.list.itemDisabledTextColor, - plainBackgroundColor: theme.list.plainBackgroundColor, - blocksBackgroundColor: theme.list.blocksBackgroundColor, - itemPlainSeparatorColor: theme.list.itemPlainSeparatorColor, - itemBlocksBackgroundColor: theme.list.itemBlocksBackgroundColor, - itemBlocksSeparatorColor: theme.list.itemBlocksSeparatorColor, - itemHighlightedBackgroundColor: theme.list.itemHighlightedBackgroundColor, - sectionHeaderTextColor: theme.list.sectionHeaderTextColor, - freeTextColor: theme.list.freeTextColor, - freeTextErrorColor: theme.list.freeTextErrorColor, - inputClearButtonColor: theme.list.inputClearButtonColor - ), - statusBarStyle: theme.rootController.statusBarStyle.style, - navigationBar: navigationBarData.theme, - keyboardAppearance: theme.rootController.keyboardColor.keyboardAppearance, - alert: AlertControllerTheme(presentationTheme: theme), - actionSheet: ActionSheetControllerTheme(presentationTheme: theme) - ), strings: WalletStrings( - primaryComponent: WalletStringsComponent( - languageCode: strings.primaryComponent.languageCode, - localizedName: strings.primaryComponent.localizedName, - pluralizationRulesCode: strings.primaryComponent.pluralizationRulesCode, - dict: strings.primaryComponent.dict - ), - secondaryComponent: strings.secondaryComponent.flatMap { component in - return WalletStringsComponent( - languageCode: component.languageCode, - localizedName: component.localizedName, - pluralizationRulesCode: component.pluralizationRulesCode, - dict: component.dict - ) - }, - groupingSeparator: strings.groupingSeparator - ), dateTimeFormat: WalletPresentationDateTimeFormat( - timeFormat: timeFormat, - dateFormat: dateFormat, - dateSeparator: presentationData.dateTimeFormat.dateSeparator, - decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, - groupingSeparator: presentationData.dateTimeFormat.groupingSeparator - ) + ), transaction: WalletTransactionTheme( + descriptionBackgroundColor: theme.chat.message.incoming.bubble.withoutWallpaper.fill, + descriptionTextColor: theme.chat.message.incoming.primaryTextColor + ), setup: WalletSetupTheme( + buttonFillColor: theme.list.itemCheckColors.fillColor, + buttonForegroundColor: theme.list.itemCheckColors.foregroundColor, + inputBackgroundColor: theme.actionSheet.inputBackgroundColor, + inputPlaceholderColor: theme.actionSheet.inputPlaceholderColor, + inputTextColor: theme.actionSheet.inputTextColor, + inputClearButtonColor: theme.actionSheet.inputClearButtonColor.withAlphaComponent(0.8) + ), + list: WalletListTheme( + itemPrimaryTextColor: theme.list.itemPrimaryTextColor, + itemSecondaryTextColor: theme.list.itemSecondaryTextColor, + itemPlaceholderTextColor: theme.list.itemPlaceholderTextColor, + itemDestructiveColor: theme.list.itemDestructiveColor, + itemAccentColor: theme.list.itemAccentColor, + itemDisabledTextColor: theme.list.itemDisabledTextColor, + plainBackgroundColor: theme.list.plainBackgroundColor, + blocksBackgroundColor: theme.list.blocksBackgroundColor, + itemPlainSeparatorColor: theme.list.itemPlainSeparatorColor, + itemBlocksBackgroundColor: theme.list.itemBlocksBackgroundColor, + itemBlocksSeparatorColor: theme.list.itemBlocksSeparatorColor, + itemHighlightedBackgroundColor: theme.list.itemHighlightedBackgroundColor, + sectionHeaderTextColor: theme.list.sectionHeaderTextColor, + freeTextColor: theme.list.freeTextColor, + freeTextErrorColor: theme.list.freeTextErrorColor, + inputClearButtonColor: theme.list.inputClearButtonColor + ), + statusBarStyle: theme.rootController.statusBarStyle.style, + navigationBar: navigationBarData.theme, + keyboardAppearance: theme.rootController.keyboardColor.keyboardAppearance, + alert: AlertControllerTheme(presentationTheme: theme), + actionSheet: ActionSheetControllerTheme(presentationTheme: theme) + ), strings: WalletStrings( + primaryComponent: WalletStringsComponent( + languageCode: strings.primaryComponent.languageCode, + localizedName: strings.primaryComponent.localizedName, + pluralizationRulesCode: strings.primaryComponent.pluralizationRulesCode, + dict: strings.primaryComponent.dict + ), + secondaryComponent: strings.secondaryComponent.flatMap { component in + return WalletStringsComponent( + languageCode: component.languageCode, + localizedName: component.localizedName, + pluralizationRulesCode: component.pluralizationRulesCode, + dict: component.dict + ) + }, + groupingSeparator: strings.groupingSeparator + ), dateTimeFormat: WalletPresentationDateTimeFormat( + timeFormat: timeFormat, + dateFormat: dateFormat, + dateSeparator: presentationData.dateTimeFormat.dateSeparator, + decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, + groupingSeparator: presentationData.dateTimeFormat.groupingSeparator + ) ) } @@ -241,11 +244,11 @@ final class WalletContextImpl: WalletContext { }) } - func pickImage(completion: @escaping (UIImage) -> Void) { + func pickImage(present: @escaping (ViewController) -> Void, completion: @escaping (UIImage) -> Void) { self.context.sharedContext.openImagePicker(context: self.context, completion: { image in completion(image) }, present: { [weak self] controller in - self?.context.sharedContext.mainWindow?.present(controller, on: .root) + present(controller) }) } } diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index e1ff263a32..49e1b0e111 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -10,6 +10,7 @@ public enum UndoOverlayContent { case revealedArchive(title: String, text: String, undo: Bool) case succeed(text: String) case emoji(path: String, text: String) + case swipeToReply(title: String, text: String) } public final class UndoOverlayController: ViewController { diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index b648b02e02..956e709334 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -137,6 +137,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.textNode.maximumNumberOfLines = 2 displayUndo = false self.originalRemainingSeconds = 5 + case let .swipeToReply(title, text): + self.iconNode = nil + self.iconCheckNode = nil + self.animationNode = AnimationNode(animation: "anim_swipereply", colors: [:], scale: 1.0) + self.animatedStickerNode = nil + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white) + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) + self.textNode.maximumNumberOfLines = 2 + displayUndo = false + self.originalRemainingSeconds = 5 } self.remainingSeconds = self.originalRemainingSeconds @@ -168,7 +178,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { case .removedChat: self.panelWrapperNode.addSubnode(self.timerTextNode) self.panelWrapperNode.addSubnode(self.statusNode) - case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji: + case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply: break } self.iconNode.flatMap(self.panelWrapperNode.addSubnode) diff --git a/submodules/UrlEscaping/Sources/UrlEscaping.swift b/submodules/UrlEscaping/Sources/UrlEscaping.swift index 84d4688fdf..d0dd0a9f00 100644 --- a/submodules/UrlEscaping/Sources/UrlEscaping.swift +++ b/submodules/UrlEscaping/Sources/UrlEscaping.swift @@ -51,7 +51,7 @@ public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": tr public func explicitUrl(_ url: String) -> String { var url = url - if !url.hasPrefix("http") && !url.hasPrefix("https") { + if !url.hasPrefix("http") && !url.hasPrefix("https") && url.range(of: "://") == nil { url = "https://\(url)" } return url diff --git a/submodules/WalletUI/Sources/WalletAmountItem.swift b/submodules/WalletUI/Sources/WalletAmountItem.swift index 6df3a1faf3..706f76b062 100644 --- a/submodules/WalletUI/Sources/WalletAmountItem.swift +++ b/submodules/WalletUI/Sources/WalletAmountItem.swift @@ -65,15 +65,20 @@ class WalletAmountItem: ListViewItem, ItemListItem { private let integralFont = Font.medium(48.0) private let fractionalFont = Font.medium(24.0) +private let iconSize = CGSize(width: 50.0, height: 50.0) +private let verticalOffset: CGFloat = -10.0 + class WalletAmountItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemNode, ItemListItemFocusableNode { private let backgroundNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode + private let containerNode: ASDisplayNode private let textNode: TextFieldNode private let iconNode: AnimatedStickerNode private let measureNode: TextNode private var item: WalletAmountItem? + private var validLayout: (CGFloat, CGFloat, CGFloat)? var tag: ItemListItemTag? { return self.item?.tag @@ -86,6 +91,8 @@ class WalletAmountItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemN self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode.isLayerBacked = true + self.containerNode = ASDisplayNode() + self.textNode = TextFieldNode() self.iconNode = AnimatedStickerNode() @@ -100,8 +107,9 @@ class WalletAmountItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemN self.clipsToBounds = false - self.addSubnode(self.textNode) - self.addSubnode(self.iconNode) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.textNode) + self.containerNode.addSubnode(self.iconNode) } override func didLoad() { @@ -122,9 +130,49 @@ class WalletAmountItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemN self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) } + private func inputFieldAsyncLayout() -> (_ item: WalletAmountItem, _ params: ListViewItemLayoutParams) -> (NSAttributedString, NSAttributedString, () -> Void) { + let makeMeasureLayout = TextNode.asyncLayout(self.measureNode) + + return { item, params in + let contentSize = CGSize(width: params.width, height: 100.0) + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets()) + + let attributedPlaceholderText = NSAttributedString(string: "0", font: integralFont, textColor: item.theme.list.itemPlaceholderTextColor) + let attributedAmountText = amountAttributedString(item.amount, integralFont: integralFont, fractionalFont: fractionalFont, color: item.theme.list.itemPrimaryTextColor) + + let (measureLayout, _) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: item.amount.isEmpty ? attributedPlaceholderText : attributedAmountText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + return (attributedPlaceholderText, attributedAmountText, { [weak self] in + if let strongSelf = self { + let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - max(31.0, measureLayout.size.width)) / 2.0 - 28.0), y: floor((layout.contentSize.height - iconSize.height) / 2.0) - 3.0 + verticalOffset), size: iconSize) + strongSelf.iconNode.updateLayout(size: iconFrame.size) + strongSelf.iconNode.frame = iconFrame + + let totalWidth = measureLayout.size.width + iconSize.width + 6.0 + let paddedWidth = layout.size.width - 32.0 + if totalWidth > paddedWidth { + let scale = paddedWidth / totalWidth + strongSelf.containerNode.transform = CATransform3DMakeScale(scale, scale, 1.0) + } else { + strongSelf.containerNode.transform = CATransform3DIdentity + } + } + }) + } + } + + private func updateInputField() { + guard let item = self.item, let validLayout = self.validLayout else { + return + } + let makeInputFieldLayout = self.inputFieldAsyncLayout() + let (_, _, inputFieldApply) = makeInputFieldLayout(item, ListViewItemLayoutParams(width: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2)) + inputFieldApply() + } + func asyncLayout() -> (_ item: WalletAmountItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let currentItem = self.item - let makeMeasureLayout = TextNode.asyncLayout(self.measureNode) + let makeInputFieldLayout = self.inputFieldAsyncLayout() return { item, params, neighbors in var updatedTheme: WalletTheme? @@ -143,16 +191,12 @@ class WalletAmountItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemN insets.top = 0.0 let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size - - let attributedPlaceholderText = NSAttributedString(string: "0", font: integralFont, textColor: item.theme.list.itemPlaceholderTextColor) - let attributedAmountText = amountAttributedString(item.amount, integralFont: integralFont, fractionalFont: fractionalFont, color: item.theme.list.itemPrimaryTextColor) - - let (measureLayout, _) = makeMeasureLayout(TextNodeLayoutArguments(attributedString: item.amount.isEmpty ? attributedPlaceholderText : attributedAmountText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (attributedPlaceholderText, attributedAmountText, inputFieldApply) = makeInputFieldLayout(item, params) return (layout, { [weak self] in if let strongSelf = self { strongSelf.item = item + strongSelf.validLayout = (params.width, params.leftInset, params.rightInset) if let _ = updatedTheme { strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor @@ -185,10 +229,7 @@ class WalletAmountItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemN strongSelf.textNode.textField.attributedText = attributedAmountText } - let iconSize = CGSize(width: 50.0, height: 50.0) - let verticalOffset: CGFloat = -10.0 - - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - 48.0) / 2.0) + verticalOffset), size: CGSize(width: max(1.0, params.width - (leftInset + rightInset) + iconSize.width - 5.0), height: 48.0)) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset - 67.0, y: floor((layout.contentSize.height - 48.0) / 2.0) + verticalOffset), size: CGSize(width: max(1.0, params.width + iconSize.width - 5.0 + 100.0), height: 48.0)) if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -207,16 +248,15 @@ class WalletAmountItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemN } strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layout.size.width - bottomStripeInset, height: separatorHeight)) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize) if strongSelf.textNode.textField.attributedPlaceholder == nil || !strongSelf.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) { strongSelf.textNode.textField.attributedPlaceholder = attributedPlaceholderText strongSelf.textNode.textField.accessibilityHint = attributedPlaceholderText.string } - let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - max(31.0, measureLayout.size.width)) / 2.0 - 28.0), y: floor((layout.contentSize.height - iconSize.height) / 2.0) - 3.0 + verticalOffset), size: iconSize) - strongSelf.iconNode.updateLayout(size: iconFrame.size) - strongSelf.iconNode.frame = iconFrame + inputFieldApply() } }) } @@ -232,6 +272,7 @@ class WalletAmountItemNode: ListViewItemNode, UITextFieldDelegate, ItemListItemN @objc private func textFieldTextChanged(_ textField: UITextField) { self.textUpdated(self.textNode.textField.text ?? "") + self.updateInputField() } @objc private func clearButtonPressed() { diff --git a/submodules/WalletUI/Sources/WalletContext.swift b/submodules/WalletUI/Sources/WalletContext.swift index 6298da1927..86905df74b 100644 --- a/submodules/WalletUI/Sources/WalletContext.swift +++ b/submodules/WalletUI/Sources/WalletContext.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import SwiftSignalKit +import Display import WalletCore public enum WalletContextGetServerSaltError { @@ -26,5 +27,5 @@ public protocol WalletContext { func shareUrl(_ url: String) func openPlatformSettings() func authorizeAccessToCamera(completion: @escaping () -> Void) - func pickImage(completion: @escaping (UIImage) -> Void) + func pickImage(present: @escaping (ViewController) -> Void, completion: @escaping (UIImage) -> Void) } diff --git a/submodules/WalletUI/Sources/WalletCreateInvoiceScreen.swift b/submodules/WalletUI/Sources/WalletCreateInvoiceScreen.swift index 09f56c37e8..597041dfe2 100644 --- a/submodules/WalletUI/Sources/WalletCreateInvoiceScreen.swift +++ b/submodules/WalletUI/Sources/WalletCreateInvoiceScreen.swift @@ -184,7 +184,7 @@ protocol WalletCreateInvoiceScreen { private final class WalletCreateInvoiceScreenImpl: ItemListController, WalletCreateInvoiceScreen { override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { - return CGSize(width: layout.size.width, height: layout.size.height - 174.0) + return CGSize(width: layout.size.width, height: min(640.0, layout.size.height)) } } diff --git a/submodules/WalletUI/Sources/WalletInfoScreen.swift b/submodules/WalletUI/Sources/WalletInfoScreen.swift index 8a2d5a095c..7357a200be 100644 --- a/submodules/WalletUI/Sources/WalletInfoScreen.swift +++ b/submodules/WalletUI/Sources/WalletInfoScreen.swift @@ -94,7 +94,7 @@ public final class WalletInfoScreen: ViewController { @objc private func settingsPressed() { if let walletInfo = self.walletInfo { - self.push(walletSettingsController(context: self.context, walletInfo: walletInfo)) + self.push(walletSettingsController(context: self.context, walletInfo: walletInfo)) } } @@ -135,24 +135,24 @@ public final class WalletInfoScreen: ViewController { } } -private final class WalletInfoBalanceNode: ASDisplayNode { +final class WalletInfoBalanceNode: ASDisplayNode { let dateTimeFormat: WalletPresentationDateTimeFormat let balanceIntegralTextNode: ImmediateTextNode let balanceFractionalTextNode: ImmediateTextNode let balanceIconNode: AnimatedStickerNode - var balance: String = " " { + var balance: (String, UIColor) = (" ", .white) { didSet { let integralString = NSMutableAttributedString() let fractionalString = NSMutableAttributedString() - if let range = self.balance.range(of: self.dateTimeFormat.decimalSeparator) { - let integralPart = String(self.balance[.. Void, receiveAction: @escaping () -> Void) { self.hasActions = hasActions - self.balanceNode = WalletInfoBalanceNode(theme: presentationData.theme, dateTimeFormat: presentationData.dateTimeFormat) + self.balanceNode = WalletInfoBalanceNode(dateTimeFormat: presentationData.dateTimeFormat) self.balanceSubtitleNode = ImmediateTextNode() self.balanceSubtitleNode.displaysAsynchronously = false @@ -856,7 +856,7 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode { private func updateCombinedState(combinedState: CombinedWalletState?, isUpdated: Bool) { self.combinedState = combinedState if let combinedState = combinedState { - self.headerNode.balanceNode.balance = formatBalanceText(max(0, combinedState.walletState.balance), decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator) + self.headerNode.balanceNode.balance = (formatBalanceText(max(0, combinedState.walletState.balance), decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator), .white) self.headerNode.balance = max(0, combinedState.walletState.balance) if self.isReady, let (layout, navigationHeight) = self.validLayout { diff --git a/submodules/WalletUI/Sources/WalletInfoTransactionItem.swift b/submodules/WalletUI/Sources/WalletInfoTransactionItem.swift index ff934c5479..d09715ee29 100644 --- a/submodules/WalletUI/Sources/WalletInfoTransactionItem.swift +++ b/submodules/WalletUI/Sources/WalletInfoTransactionItem.swift @@ -113,6 +113,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode { private let iconNode: ASImageNode private let textNode: TextNode private let descriptionNode: TextNode + private let feesNode: TextNode private let dateNode: TextNode private var statusNode: StatusClockNode? @@ -157,6 +158,11 @@ class WalletInfoTransactionItemNode: ListViewItemNode { self.descriptionNode.contentMode = .left self.descriptionNode.contentsScale = UIScreen.main.scale + self.feesNode = TextNode() + self.feesNode.isUserInteractionEnabled = false + self.feesNode.contentMode = .left + self.feesNode.contentsScale = UIScreen.main.scale + self.dateNode = TextNode() self.dateNode.isUserInteractionEnabled = false self.dateNode.contentMode = .left @@ -175,6 +181,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode { self.addSubnode(self.directionNode) self.addSubnode(self.textNode) self.addSubnode(self.descriptionNode) + self.addSubnode(self.feesNode) self.addSubnode(self.dateNode) self.addSubnode(self.activateArea) @@ -186,6 +193,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode { let makeDirectionLayout = TextNode.asyncLayout(self.directionNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode) + let makeFeesLayout = TextNode.asyncLayout(self.feesNode) let makeDateLayout = TextNode.asyncLayout(self.dateNode) let currentItem = self.item @@ -269,18 +277,15 @@ class WalletInfoTransactionItemNode: ListViewItemNode { } } + var feeText: String = "" let dateText: String switch item.walletTransaction { case let .completed(transaction): let fee = transaction.storageFee + transaction.otherFee if fee != 0 { - let feeText = item.strings.Wallet_Info_TransactionBlockchainFee(formatBalanceText(-fee, decimalSeparator: item.dateTimeFormat.decimalSeparator)).0 - if !description.isEmpty { - description.append("\n") - } - description += "\(feeText)" + feeText = item.strings.Wallet_Info_TransactionBlockchainFee(formatBalanceText(-fee, decimalSeparator: item.dateTimeFormat.decimalSeparator)).0 } - dateText = stringForMessageTimestamp(timestamp: Int32(clamping: transaction.timestamp), dateTimeFormat: item.dateTimeFormat) + dateText = stringForMessageTimestamp(timestamp: Int32(clamping: transaction.timestamp), dateTimeFormat: item.dateTimeFormat) case let .pending(transaction): dateText = stringForMessageTimestamp(timestamp: Int32(clamping: transaction.timestamp), dateTimeFormat: item.dateTimeFormat) } @@ -307,7 +312,9 @@ class WalletInfoTransactionItemNode: ListViewItemNode { let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: description, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: description, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (feesLayout, feesApply) = makeFeesLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: feeText, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var contentSize: CGSize var insets: UIEdgeInsets @@ -328,6 +335,9 @@ class WalletInfoTransactionItemNode: ListViewItemNode { if !descriptionLayout.size.width.isZero { contentSize.height += descriptionLayout.size.height + textSpacing } + if !feesLayout.size.width.isZero { + contentSize.height += feesLayout.size.height + textSpacing + } insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) var topHighlightInset: CGFloat = 0.0 if dateHeaderAtBottom, let header = item.header { @@ -356,6 +366,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode { let _ = titleApply() let _ = textApply() let _ = descriptionApply() + let _ = feesApply() let _ = dateApply() let _ = directionApply() @@ -385,7 +396,9 @@ class WalletInfoTransactionItemNode: ListViewItemNode { let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: textLayout.size) strongSelf.textNode.frame = textFrame - strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textFrame.maxY + textSpacing), size: descriptionLayout.size) + let descriptionFrame = CGRect(origin: CGPoint(x: leftInset, y: textFrame.maxY + textSpacing), size: descriptionLayout.size) + strongSelf.descriptionNode.frame = descriptionFrame + strongSelf.feesNode.frame = CGRect(origin: CGPoint(x: leftInset, y: descriptionFrame.maxY + textSpacing), size: feesLayout.size) let dateFrame = CGRect(origin: CGPoint(x: params.width - leftInset - dateLayout.size.width, y: topInset), size: dateLayout.size) strongSelf.dateNode.frame = dateFrame diff --git a/submodules/WalletUI/Sources/WalletPasscodeScreen.swift b/submodules/WalletUI/Sources/WalletPasscodeScreen.swift deleted file mode 100644 index 5be1703679..0000000000 --- a/submodules/WalletUI/Sources/WalletPasscodeScreen.swift +++ /dev/null @@ -1,248 +0,0 @@ -/*import Foundation -import UIKit -import AppBundle -import AsyncDisplayKit -import Display -import AnimationUI -import SwiftSignalKit -import OverlayStatusController -import PasscodeInputFieldNode - -public enum WalletPasscodeMode { - case setup - case authorizeTransfer(WalletInfo, String, Int64, Data) -} - -public final class WalletPasscodeScreen: ViewController { - private let context: WalletContext - private var presentationData: WalletPresentationData - private let mode: WalletPasscodeMode - private let randomId: Int64 - - public init(context: WalletContext, mode: WalletPasscodeMode) { - self.context = context - self.mode = mode - - self.randomId = arc4random64() - - self.presentationData = context.presentationData - - let defaultNavigationPresentationData = NavigationBarPresentationData(WalletTheme: self.presentationData.theme, WalletStrings: self.presentationData.strings) - let navigationBarTheme = NavigationBarTheme(buttonColor: defaultNavigationPresentationData.theme.buttonColor, disabledButtonColor: defaultNavigationPresentationData.theme.disabledButtonColor, primaryTextColor: defaultNavigationPresentationData.theme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultNavigationPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: defaultNavigationPresentationData.theme.badgeStrokeColor, badgeTextColor: defaultNavigationPresentationData.theme.badgeTextColor) - - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: defaultNavigationPresentationData.strings)) - - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.navigationPresentation = .modalInLargeLayout - self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.navigationBar?.intrinsicCanTransitionInline = false - - self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.backPressed)), animated: false) - - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Back, style: .plain, target: nil, action: nil) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func backPressed() { - self.view.endEditing(true) - self.dismiss() - } - - override public func loadDisplayNode() { - self.displayNode = WalletPasscodeScreenNode(account: self.context.account, WalletPresentationData: self.presentationData, mode: self.mode, proceed: { [weak self] in - guard let strongSelf = self else { - return - } - switch strongSelf.mode { - case .setup: - break - case let .authorizeTransfer(walletInfo, address, amount, comment): - if let navigationController = strongSelf.navigationController as? NavigationController { - var controllers = navigationController.viewControllers - controllers = controllers.filter { controller in - if controller is WalletSplashScreen { - return false - } - if controller is WalletSendScreen { - return false - } - if controller is WalletPasscodeScreen { - return false - } - return true - } - controllers.append(WalletSplashScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .sending(walletInfo, address, amount, comment, strongSelf.randomId, Data()), walletCreatedPreloadState: nil)) - strongSelf.view.endEditing(true) - navigationController.setViewControllers(controllers, animated: true) - } - } - }, requestBiometrics: { - - }) - - self.displayNodeDidLoad() - } - - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - (self.displayNode as! WalletPasscodeScreenNode).activateInput() - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - self.view.disablesInteractiveTransitionGestureRecognizer = true - - (self.displayNode as! WalletPasscodeScreenNode).activateInput() - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - (self.displayNode as! WalletPasscodeScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) - } -} - -private final class WalletPasscodeScreenNode: ViewControllerTracingNode { - private var presentationData: WalletPresentationData - private let mode: WalletPasscodeMode - private let requestBiometrics: () -> Void - - private let iconNode: ASImageNode - private let animationNode: AnimatedStickerNode - private let titleNode: ImmediateTextNode - private let biometricsActionTitleNode: ImmediateTextNode - private let biometricsActionButtonNode: HighlightTrackingButtonNode - private let inputFieldNode: PasscodeInputFieldNode - - private let hapticFeedback = HapticFeedback() - - init(account: Account, presentationData: WalletPresentationData, mode: WalletPasscodeMode, proceed: @escaping () -> Void, requestBiometrics: @escaping () -> Void) { - self.presentationData = WalletPresentationData - self.mode = mode - self.requestBiometrics = requestBiometrics - - self.iconNode = ASImageNode() - self.iconNode.displayWithoutProcessing = true - self.iconNode.displaysAsynchronously = false - - self.animationNode = AnimatedStickerNode() - - let title: String - let biometricsActionText: String - - title = "Enter Passcode" - biometricsActionText = "Use Face ID" - - self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/PasscodeIcon") - - self.titleNode = ImmediateTextNode() - self.titleNode.displaysAsynchronously = false - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(32.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.titleNode.maximumNumberOfLines = 0 - self.titleNode.textAlignment = .center - - self.biometricsActionTitleNode = ImmediateTextNode() - self.biometricsActionTitleNode.displaysAsynchronously = false - self.biometricsActionTitleNode.attributedText = NSAttributedString(string: biometricsActionText, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemAccentColor, paragraphAlignment: .center) - self.biometricsActionTitleNode.textAlignment = .center - - self.biometricsActionButtonNode = HighlightTrackingButtonNode() - - self.inputFieldNode = PasscodeInputFieldNode(color: self.presentationData.theme.list.itemPrimaryTextColor, accentColor: self.presentationData.theme.list.itemAccentColor, fieldType: .digits4, keyboardAppearance: self.presentationData.theme.rootController.keyboardColor.keyboardAppearance) - - super.init() - - self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - - self.addSubnode(self.iconNode) - self.addSubnode(self.animationNode) - self.addSubnode(self.titleNode) - self.addSubnode(self.biometricsActionTitleNode) - self.addSubnode(self.biometricsActionButtonNode) - self.addSubnode(self.inputFieldNode) - - self.biometricsActionButtonNode.highligthedChanged = { [weak self] highlighted in - guard let strongSelf = self else { - return - } - if highlighted { - strongSelf.biometricsActionTitleNode.layer.removeAnimation(forKey: "opacity") - strongSelf.biometricsActionTitleNode.alpha = 0.4 - } else { - strongSelf.biometricsActionTitleNode.alpha = 1.0 - strongSelf.biometricsActionTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - - self.biometricsActionButtonNode.addTarget(self, action: #selector(self.biometricsActionPressed), forControlEvents: .touchUpInside) - - self.inputFieldNode.complete = { [weak self] passcode in - if passcode == "1111" { - proceed() - } else { - self?.animateError() - } - } - } - - @objc private func biometricsActionPressed() { - self.requestBiometrics() - } - - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let sideInset: CGFloat = 32.0 - let buttonSideInset: CGFloat = 48.0 - let iconSpacing: CGFloat = 21.0 - let titleSpacing: CGFloat = 60.0 - let biometricsSpacing: CGFloat = 44.0 - let buttonHeight: CGFloat = 50.0 - let inputFieldHeight: CGFloat = 34.0 - - let iconSize = self.iconNode.image?.size ?? CGSize(width: 140.0, height: 140.0) - var iconOffset = CGPoint() - - let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) - let biometricsActionSize = self.biometricsActionTitleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) - - let insets = layout.insets(options: [.input]) - let contentHeight = iconSize.height + iconSpacing + titleSize.height + titleSpacing + inputFieldHeight - let contentVerticalOrigin = floor((layout.size.height - contentHeight - iconSize.height / 2.0 - insets.bottom) / 2.0) - - let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: contentVerticalOrigin), size: iconSize).offsetBy(dx: iconOffset.x, dy: iconOffset.y) - transition.updateFrameAdditive(node: self.iconNode, frame: iconFrame) - self.animationNode.updateLayout(size: iconFrame.size) - transition.updateFrameAdditive(node: self.animationNode, frame: iconFrame) - let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleSize) - transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) - - let inputFieldFrame = self.inputFieldNode.updateLayout(layout: layout, topOffset: titleFrame.maxY + titleSpacing, transition: transition) - transition.updateFrame(node: self.inputFieldNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let minimalBottomInset: CGFloat = 60.0 - let bottomInset = layout.intrinsicInsets.bottom + max(minimalBottomInset, biometricsActionSize.height + biometricsSpacing * 2.0) - - if !biometricsActionSize.width.isZero { - let biometricsActionFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - biometricsActionSize.width) / 2.0), y: inputFieldFrame.maxY + floor((layout.size.height - insets.bottom - inputFieldFrame.maxY - biometricsActionSize.height) / 2.0)), size: biometricsActionSize) - transition.updateFrameAdditive(node: self.biometricsActionTitleNode, frame: biometricsActionFrame) - transition.updateFrame(node: self.biometricsActionButtonNode, frame: biometricsActionFrame.insetBy(dx: -10.0, dy: -10.0)) - } - } - - func activateInput() { - self.inputFieldNode.activateInput() - - UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: self.titleNode.attributedText?.string) - } - - func animateError() { - self.inputFieldNode.reset() - self.inputFieldNode.layer.addShakeAnimation(amplitude: -30.0, duration: 0.5, count: 6, decay: true) - - self.hapticFeedback.error() - } -} -*/ diff --git a/submodules/WalletUI/Sources/WalletPresentationData.swift b/submodules/WalletUI/Sources/WalletPresentationData.swift index 0c4ac29cb6..23546e32d8 100644 --- a/submodules/WalletUI/Sources/WalletPresentationData.swift +++ b/submodules/WalletUI/Sources/WalletPresentationData.swift @@ -49,6 +49,19 @@ public final class WalletInfoTheme { } } +public final class WalletTransactionTheme { + public let descriptionBackgroundColor: UIColor + public let descriptionTextColor: UIColor + + public init( + descriptionBackgroundColor: UIColor, + descriptionTextColor: UIColor + ) { + self.descriptionBackgroundColor = descriptionBackgroundColor + self.descriptionTextColor = descriptionTextColor + } +} + public final class WalletSetupTheme { public let buttonFillColor: UIColor public let buttonForegroundColor: UIColor @@ -131,6 +144,7 @@ public final class WalletListTheme { public final class WalletTheme: Equatable { public let info: WalletInfoTheme + public let transaction: WalletTransactionTheme public let setup: WalletSetupTheme public let list: WalletListTheme public let statusBarStyle: StatusBarStyle @@ -141,8 +155,9 @@ public final class WalletTheme: Equatable { private let resourceCache = WalletThemeResourceCache() - public init(info: WalletInfoTheme, setup: WalletSetupTheme, list: WalletListTheme, statusBarStyle: StatusBarStyle, navigationBar: NavigationBarTheme, keyboardAppearance: UIKeyboardAppearance, alert: AlertControllerTheme, actionSheet: ActionSheetControllerTheme) { + public init(info: WalletInfoTheme, transaction: WalletTransactionTheme, setup: WalletSetupTheme, list: WalletListTheme, statusBarStyle: StatusBarStyle, navigationBar: NavigationBarTheme, keyboardAppearance: UIKeyboardAppearance, alert: AlertControllerTheme, actionSheet: ActionSheetControllerTheme) { self.info = info + self.transaction = transaction self.setup = setup self.list = list self.statusBarStyle = statusBarStyle diff --git a/submodules/WalletUI/Sources/WalletQrCodeItem.swift b/submodules/WalletUI/Sources/WalletQrCodeItem.swift deleted file mode 100644 index 3d1eb1423d..0000000000 --- a/submodules/WalletUI/Sources/WalletQrCodeItem.swift +++ /dev/null @@ -1,167 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit -import QrCode - -class WalletQrCodeItem: ListViewItem, ItemListItem { - let theme: WalletTheme - let address: String - let sectionId: ItemListSectionId - let style: ItemListStyle - let action: (() -> Void)? - let longTapAction: (() -> Void)? - public let isAlwaysPlain: Bool = true - - init(theme: WalletTheme, address: String, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, longTapAction: @escaping () -> Void) { - self.theme = theme - self.address = address - self.sectionId = sectionId - self.style = style - self.action = action - self.longTapAction = longTapAction - } - - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { - async { - let node = WalletQrCodeItemNode() - let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - - node.contentSize = layout.contentSize - node.insets = layout.insets - - Queue.mainQueue().async { - completion(node, { - return (nil, { _ in apply() }) - }) - } - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { - Queue.mainQueue().async { - if let nodeValue = node() as? WalletQrCodeItemNode { - let makeLayout = nodeValue.asyncLayout() - - async { - let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) - Queue.mainQueue().async { - completion(layout, { _ in - apply() - }) - } - } - } - } - } -} - -class WalletQrCodeItemNode: ListViewItemNode { - private let imageNode: TransformImageNode - - private var item: WalletQrCodeItem? - - var tag: Any? { - return self.item?.tag - } - - init() { - self.imageNode = TransformImageNode() - - super.init(layerBacked: false, dynamicBounce: false) - - self.addSubnode(self.imageNode) - } - - override func didLoad() { - super.didLoad() - - let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) - recognizer.tapActionAtPoint = { [weak self] point in - return .waitForSingleTap - } - recognizer.highlight = { [weak self] point in - self?.imageNode.alpha = point != nil ? 0.4 : 1.0 - } - self.view.addGestureRecognizer(recognizer) - } - - @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { - switch recognizer.state { - case .ended: - if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { - switch gesture { - case .tap: - self.item?.action?() - case .longTap: - self.item?.longTapAction?() - default: - break - } - } - default: - break - } - } - - func asyncLayout() -> (_ item: WalletQrCodeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { - let makeImageLayout = self.imageNode.asyncLayout() - - let currentItem = self.item - - return { item, params, neighbors in - var updatedTheme: WalletTheme? - var updatedAddress: String? - - if currentItem?.theme !== item.theme { - updatedTheme = item.theme - } - - if currentItem?.address != item.address || updatedTheme != nil { - updatedAddress = item.address - } - - let contentSize: CGSize - let insets: UIEdgeInsets - let separatorHeight = UIScreenPixel - - let inset: CGFloat = 0.0 - var imageSize = CGSize(width: 128.0, height: 128.0) - let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) - - switch item.style { - case .plain: - contentSize = CGSize(width: params.width, height: imageSize.height + 30.0) - insets = itemListNeighborsPlainInsets(neighbors) - case .blocks: - contentSize = CGSize(width: params.width, height: imageSize.height + 30.0) - insets = itemListNeighborsGroupedInsets(neighbors) - } - - let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - - return (layout, { [weak self] in - if let strongSelf = self { - strongSelf.item = item - - if let updatedAddress = updatedAddress { - strongSelf.imageNode.setSignal(qrCode(string: updatedAddress, color: item.theme.list.itemPrimaryTextColor.withAlphaComponent(0.77), backgroundColor: item.theme.list.blocksBackgroundColor, icon: .custom(UIImage(bundleImageName: "Wallet/QrGem"))), attemptSynchronously: true) - } - - let _ = imageApply() - - strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: (params.width - imageSize.width) / 2.0, y: 0.0), size: imageSize) - } - }) - } - } - - override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { - self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - } - - override func animateRemoved(_ currentTimestamp: Double, duration: Double) { - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) - } -} diff --git a/submodules/WalletUI/Sources/WalletQrScanScreen.swift b/submodules/WalletUI/Sources/WalletQrScanScreen.swift index c2a8c72e2d..e9df176be9 100644 --- a/submodules/WalletUI/Sources/WalletQrScanScreen.swift +++ b/submodules/WalletUI/Sources/WalletQrScanScreen.swift @@ -118,8 +118,10 @@ public final class WalletQrScanScreen: ViewController { guard let strongSelf = self else { return } - strongSelf.context.pickImage(completion: { image in - let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy:CIDetectorAccuracyHigh])! + strongSelf.context.pickImage(present: { c in + strongSelf.push(c) + }, completion: { image in + let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])! if let ciImage = CIImage(image: image) { var options: [String: Any] if ciImage.properties.keys.contains((kCGImagePropertyOrientation as String)) { diff --git a/submodules/WalletUI/Sources/WalletQrViewScreen.swift b/submodules/WalletUI/Sources/WalletQrViewScreen.swift deleted file mode 100644 index 8a9bb17ed2..0000000000 --- a/submodules/WalletUI/Sources/WalletQrViewScreen.swift +++ /dev/null @@ -1,143 +0,0 @@ -import Foundation -import UIKit -import SwiftSignalKit -import AppBundle -import AsyncDisplayKit -import Display -import QrCode -import AnimatedStickerNode - -public final class WalletQrViewScreen: ViewController { - private let context: WalletContext - private let invoice: String - private var presentationData: WalletPresentationData - - private var previousScreenBrightness: CGFloat? - private var displayLinkAnimator: DisplayLinkAnimator? - private let idleTimerExtensionDisposable: Disposable - - public init(context: WalletContext, invoice: String) { - self.context = context - self.invoice = invoice - - self.presentationData = context.presentationData - - let defaultTheme = self.presentationData.theme.navigationBar - let navigationBarTheme = NavigationBarTheme(buttonColor: defaultTheme.buttonColor, disabledButtonColor: defaultTheme.disabledButtonColor, primaryTextColor: defaultTheme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultTheme.badgeBackgroundColor, badgeStrokeColor: defaultTheme.badgeStrokeColor, badgeTextColor: defaultTheme.badgeTextColor) - - self.idleTimerExtensionDisposable = context.idleTimerExtension() - - super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Wallet_Navigation_Back, close: self.presentationData.strings.Wallet_Navigation_Close))) - - self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - self.navigationBar?.intrinsicCanTransitionInline = false - - self.title = self.presentationData.strings.Wallet_Qr_Title - - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Wallet_Navigation_Back, style: .plain, target: nil, action: nil) - - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.shareButtonPressed)) - } - - deinit { - self.idleTimerExtensionDisposable.dispose() - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func loadDisplayNode() { - self.displayNode = WalletQrViewScreenNode(context: self.context, presentationData: self.presentationData, message: self.invoice) - - self.displayNodeDidLoad() - } - - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - let screenBrightness = UIScreen.main.brightness - if screenBrightness < 0.85 { - self.previousScreenBrightness = screenBrightness - self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.5, from: screenBrightness, to: 0.85, update: { value in - UIScreen.main.brightness = value - }, completion: { - self.displayLinkAnimator = nil - }) - } - } - - public override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - let screenBrightness = UIScreen.main.brightness - if let previousScreenBrightness = self.previousScreenBrightness, screenBrightness > previousScreenBrightness { - self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2, from: screenBrightness, to: previousScreenBrightness, update: { value in - UIScreen.main.brightness = value - }, completion: { - self.displayLinkAnimator = nil - }) - } - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - (self.displayNode as! WalletQrViewScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) - } - - @objc private func shareButtonPressed() { - //shareInvoiceQrCode(context: self.context, invoice: self.invoice) - } -} - -private final class WalletQrViewScreenNode: ViewControllerTracingNode { - private var presentationData: WalletPresentationData - private let invoice: String - - private let imageNode: TransformImageNode - private let iconNode: AnimatedStickerNode - - init(context: WalletContext, presentationData: WalletPresentationData, message: String) { - self.presentationData = presentationData - self.invoice = message - - self.imageNode = TransformImageNode() - self.imageNode.clipsToBounds = true - self.imageNode.cornerRadius = 14.0 - - self.iconNode = AnimatedStickerNode() - if let path = getAppBundle().path(forResource: "WalletIntroStatic", ofType: "tgs") { - self.iconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 240, height: 240, mode: .direct) - self.iconNode.visibility = true - } - - super.init() - - self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - - self.addSubnode(self.imageNode) - self.addSubnode(self.iconNode) - - self.imageNode.setSignal(qrCode(string: self.invoice, color: .black, backgroundColor: .white, icon: .cutout), attemptSynchronously: true) - } - - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { - let makeImageLayout = self.imageNode.asyncLayout() - - let imageSide = layout.size.width - 48.0 * 2.0 - var imageSize = CGSize(width: imageSide, height: imageSide) - let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) - - let _ = imageApply() - - let imageFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: floor((layout.size.height - imageSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: imageSize) - transition.updateFrame(node: self.imageNode, frame: imageFrame) - - let iconSide = floor(imageSide * 0.24) - let iconSize = CGSize(width: iconSide, height: iconSide) - self.iconNode.updateLayout(size: iconSize) - transition.updateBounds(node: self.iconNode, bounds: CGRect(origin: CGPoint(), size: iconSize)) - transition.updatePosition(node: self.iconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0)) - } -} diff --git a/submodules/WalletUI/Sources/WalletReceiveScreen.swift b/submodules/WalletUI/Sources/WalletReceiveScreen.swift index f10be88a48..9b98be4e05 100644 --- a/submodules/WalletUI/Sources/WalletReceiveScreen.swift +++ b/submodules/WalletUI/Sources/WalletReceiveScreen.swift @@ -42,8 +42,6 @@ final class WalletReceiveScreen: ViewController { private let mode: WalletReceiveScreenMode private var presentationData: WalletPresentationData - private var previousScreenBrightness: CGFloat? - private var displayLinkAnimator: DisplayLinkAnimator? private let idleTimerExtensionDisposable: Disposable public init(context: WalletContext, mode: WalletReceiveScreenMode) { @@ -89,38 +87,26 @@ final class WalletReceiveScreen: ViewController { } self?.push(walletCreateInvoiceScreen(context: strongSelf.context, address: strongSelf.mode.address)) } + (self.displayNode as! WalletReceiveScreenNode).displayCopyContextMenu = { [weak self] node, frame, text in + guard let strongSelf = self else { + return + } + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Wallet_ContextMenuCopy, accessibilityLabel: strongSelf.presentationData.strings.Wallet_ContextMenuCopy), action: { + UIPasteboard.general.string = text + })]) + strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (node, frame.insetBy(dx: 0.0, dy: -2.0), strongSelf.displayNode, strongSelf.displayNode.view.bounds) + } else { + return nil + } + })) + } self.displayNodeDidLoad() } - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - let screenBrightness = UIScreen.main.brightness - if screenBrightness < 0.85 { - self.previousScreenBrightness = screenBrightness - self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.5, from: screenBrightness, to: 0.85, update: { value in - UIScreen.main.brightness = value - }, completion: { - self.displayLinkAnimator = nil - }) - } - } - - public override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - let screenBrightness = UIScreen.main.brightness - if let previousScreenBrightness = self.previousScreenBrightness, screenBrightness > previousScreenBrightness { - self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2, from: screenBrightness, to: previousScreenBrightness, update: { value in - UIScreen.main.brightness = value - }, completion: { - self.displayLinkAnimator = nil - }) - } - } - override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { - return CGSize(width: layout.size.width, height: layout.size.height - 174.0) + return CGSize(width: layout.size.width, height: min(640.0, layout.size.height)) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -172,6 +158,7 @@ private final class WalletReceiveScreenNode: ViewControllerTracingNode { private let secondaryButtonNode: HighlightableButtonNode var openCreateInvoice: (() -> Void)? + var displayCopyContextMenu: ((ASDisplayNode, CGRect, String) -> Void)? init(context: WalletContext, presentationData: WalletPresentationData, mode: WalletReceiveScreenMode) { self.context = context @@ -234,21 +221,16 @@ private final class WalletReceiveScreenNode: ViewControllerTracingNode { } let textFont = Font.regular(16.0) - let addressFont = Font.monospace(17.0) let textColor = self.presentationData.theme.list.itemPrimaryTextColor let secondaryTextColor = self.presentationData.theme.list.itemSecondaryTextColor let url = urlForMode(self.mode) switch self.mode { case let .receive(address): self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Wallet_Receive_ShareUrlInfo, font: textFont, textColor: secondaryTextColor) - self.urlTextNode.attributedText = NSAttributedString(string: formatAddress(url + " "), font: addressFont, textColor: textColor, paragraphAlignment: .justified) self.buttonNode.title = self.presentationData.strings.Wallet_Receive_ShareAddress self.secondaryButtonNode.setTitle(self.presentationData.strings.Wallet_Receive_CreateInvoice, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) case let .invoice(address, amount, comment): self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Wallet_Receive_ShareUrlInfo, font: textFont, textColor: secondaryTextColor, paragraphAlignment: .center) - - let sliced = String(url.enumerated().map { $0 > 0 && $0 % 32 == 0 ? ["\n", $1] : [$1]}.joined()) - self.urlTextNode.attributedText = NSAttributedString(string: sliced, font: addressFont, textColor: textColor, paragraphAlignment: .justified) self.buttonNode.title = self.presentationData.strings.Wallet_Receive_ShareInvoiceUrl } @@ -258,6 +240,32 @@ private final class WalletReceiveScreenNode: ViewControllerTracingNode { self.secondaryButtonNode.addTarget(self, action: #selector(createInvoicePressed), forControlEvents: .touchUpInside) } + override func didLoad() { + super.didLoad() + + let addressGestureRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapAddressGesture(_:))) + addressGestureRecognizer.tapActionAtPoint = { [weak self] point in + return .waitForSingleTap + } + self.urlTextNode.view.addGestureRecognizer(addressGestureRecognizer) + } + + @objc func tapLongTapOrDoubleTapAddressGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .longTap: + self.displayCopyContextMenu?(self, self.urlTextNode.frame, urlForMode(self.mode)) + default: + break + } + } + default: + break + } + } + @objc private func qrPressed() { shareInvoiceQrCode(context: self.context, invoice: urlForMode(self.mode)) } @@ -293,8 +301,30 @@ private final class WalletReceiveScreenNode: ViewControllerTracingNode { transition.updateBounds(node: self.qrIconNode, bounds: CGRect(origin: CGPoint(), size: iconSize)) transition.updatePosition(node: self.qrIconNode, position: imageFrame.center.offsetBy(dx: 0.0, dy: -1.0)) - let urlTextSize = self.urlTextNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.urlTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - urlTextSize.width) / 2.0), y: imageFrame.maxY + 25.0), size: urlTextSize)) + if self.urlTextNode.attributedText?.string.isEmpty ?? true { + var url = urlForMode(self.mode) + if case .receive = self.mode { + url = url + "?" + } + + let addressFont: UIFont + let countRatio: CGFloat + if layout.size.width == 320.0 { + addressFont = Font.monospace(16.0) + countRatio = 0.0999 + } else { + addressFont = Font.monospace(17.0) + countRatio = 0.0853 + } + let count = min(url.count / 2, Int(ceil(min(layout.size.width, layout.size.height) * countRatio))) + let sliced = String(url.enumerated().map { $0 > 0 && $0 % count == 0 ? ["\n", $1] : [$1]}.joined()) + + self.urlTextNode.attributedText = NSAttributedString(string: sliced, font: addressFont, textColor: self.presentationData.theme.list.itemPrimaryTextColor, paragraphAlignment: .justified) + } + + let addressInset: CGFloat = 12.0 + let urlTextSize = self.urlTextNode.updateLayout(CGSize(width: layout.size.width - addressInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + transition.updateFrame(node: self.urlTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - urlTextSize.width) / 2.0), y: imageFrame.maxY + 23.0), size: urlTextSize)) let buttonSideInset: CGFloat = 16.0 let bottomInset = insets.bottom + 10.0 diff --git a/submodules/WalletUI/Sources/WalletSendScreen.swift b/submodules/WalletUI/Sources/WalletSendScreen.swift index 69e4996518..b46dc0152a 100644 --- a/submodules/WalletUI/Sources/WalletSendScreen.swift +++ b/submodules/WalletUI/Sources/WalletSendScreen.swift @@ -336,9 +336,9 @@ public func walletSendScreen(context: WalletContext, randomId: Int64, walletInfo popImpl?() if let updatedState = updatedState { if updatedState.amount.isEmpty { - selectNextInputItemImpl?(WalletSendScreenEntryTag.address) - } else if updatedState.comment.isEmpty { selectNextInputItemImpl?(WalletSendScreenEntryTag.amount) + } else if updatedState.comment.isEmpty { + selectNextInputItemImpl?(WalletSendScreenEntryTag.comment) } } })) diff --git a/submodules/WalletUI/Sources/WalletTransactionInfoScreen.swift b/submodules/WalletUI/Sources/WalletTransactionInfoScreen.swift index d2323372d5..d2649670a4 100644 --- a/submodules/WalletUI/Sources/WalletTransactionInfoScreen.swift +++ b/submodules/WalletUI/Sources/WalletTransactionInfoScreen.swift @@ -202,7 +202,12 @@ final class WalletTransactionInfoScreen: ViewController { } var string = NSMutableAttributedString(string: "Blockchain validators collect a tiny fee for storing information about your decentralized wallet and for processing your transactions. More info", font: Font.regular(14.0), textColor: .white, paragraphAlignment: .center) string.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor(rgb: 0x6bb2ff), range: NSMakeRange(string.string.count - 10, 10)) - let controller = TooltipController(content: .attributedText(string), timeout: 3.0, dismissByTapOutside: true, dismissByTapOutsideSource: false, dismissImmediatelyOnLayoutUpdate: false) + let controller = TooltipController(content: .attributedText(string), timeout: 3.0, dismissByTapOutside: true, dismissByTapOutsideSource: false, dismissImmediatelyOnLayoutUpdate: false, arrowOnBottom: false) + controller.dismissed = { [weak self] tappedInside in + if let strongSelf = self, tappedInside { + strongSelf.context.openUrl(strongSelf.presentationData.strings.Wallet_TransactionInfo_FeeInfoURL) + } + } strongSelf.present(controller, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { if let strongSelf = self { return (node.view, rect.insetBy(dx: 0.0, dy: -4.0)) @@ -210,20 +215,55 @@ final class WalletTransactionInfoScreen: ViewController { return nil })) } + (self.displayNode as! WalletTransactionInfoScreenNode).displayCopyContextMenu = { [weak self] node, frame, text in + guard let strongSelf = self else { + return + } + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Wallet_ContextMenuCopy, accessibilityLabel: strongSelf.presentationData.strings.Wallet_ContextMenuCopy), action: { + UIPasteboard.general.string = text + })]) + strongSelf.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + if let strongSelf = self { + return (node, frame.insetBy(dx: 0.0, dy: -2.0), strongSelf.displayNode, strongSelf.displayNode.view.bounds) + } else { + return nil + } + })) + } self.displayNodeDidLoad() } private let measureTextNode = TextNode() override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? { + let insets = layout.insets(options: []) + + let minHeight: CGFloat = 424.0 + let maxHeight: CGFloat = min(596.0, layout.size.height) + let text = NSAttributedString(string: extractDescription(self.walletTransaction), font: Font.regular(17.0), textColor: .black) let makeTextLayout = TextNode.asyncLayout(self.measureTextNode) let (textLayout, _) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: layout.size.width - 36.0 * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - var textHeight = textLayout.size.height - if textHeight > 0.0 { - textHeight += 24.0 + + var resultHeight = minHeight + if textLayout.size.height > 0.0 { + let textHeight = textLayout.size.height + 24.0 + let minOverscroll: CGFloat = 42.0 + let maxOverscroll: CGFloat = 148.0 + + let contentHeight = minHeight + textHeight + let difference = contentHeight - maxHeight + if difference < 0.0 { + resultHeight = contentHeight + } else if difference > maxOverscroll { + resultHeight = maxHeight + } else if difference > minOverscroll { + resultHeight = maxHeight - (maxOverscroll - difference) + } else { + resultHeight = maxHeight - (minOverscroll - difference) + } } - let insets = layout.insets(options: []) - return CGSize(width: layout.size.width, height: 428.0 + insets.bottom + textHeight) + + return CGSize(width: layout.size.width, height: resultHeight + insets.bottom) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -237,33 +277,36 @@ final class WalletTransactionInfoScreen: ViewController { } } -private let integralFont = Font.medium(48.0) +private let amountFont = Font.medium(48.0) private let fractionalFont = Font.medium(24.0) -private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode { +private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: WalletContext private var presentationData: WalletPresentationData private let walletTransaction: WalletInfoTransaction private let incoming: Bool - + private let titleNode: ImmediateTextNode private let timeNode: ImmediateTextNode - - private let amountNode: ImmediateTextNode - private let iconNode: AnimatedStickerNode + private let navigationBackgroundNode: ASDisplayNode + private let navigationSeparatorNode: ASDisplayNode + private let scrollNode: ASScrollNode + private let amountNode: WalletInfoBalanceNode private let activateArea: AccessibilityAreaNode private let feesNode: ImmediateTextNode + private let feesInfoIconNode: ASImageNode private let feesButtonNode: ASButtonNode - private let commentBackgroundNode: ASImageNode private let commentTextNode: ImmediateTextNode - + private let commentSeparatorNode: ASDisplayNode private let addressTextNode: ImmediateTextNode - private let buttonNode: SolidRoundedButtonNode + private var validLayout: (ContainerViewLayout, CGFloat)? + var send: ((String) -> Void)? var displayFeesTooltip: ((ASDisplayNode, CGRect) -> Void)? + var displayCopyContextMenu: ((ASDisplayNode, CGRect, String) -> Void)? init(context: WalletContext, presentationData: WalletPresentationData, walletTransaction: WalletInfoTransaction) { self.context = context @@ -278,29 +321,39 @@ private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode { self.timeNode.textAlignment = .center self.timeNode.maximumNumberOfLines = 1 - self.amountNode = ImmediateTextNode() - self.amountNode.textAlignment = .center - self.amountNode.maximumNumberOfLines = 1 + self.navigationBackgroundNode = ASDisplayNode() + self.navigationBackgroundNode.backgroundColor = self.presentationData.theme.navigationBar.backgroundColor + self.navigationBackgroundNode.alpha = 0.0 + self.navigationSeparatorNode = ASDisplayNode() + self.navigationSeparatorNode.backgroundColor = self.presentationData.theme.navigationBar.separatorColor - self.iconNode = AnimatedStickerNode() - if let path = getAppBundle().path(forResource: "WalletIntroStatic", ofType: "tgs") { - self.iconNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 120, height: 120, mode: .direct) - self.iconNode.visibility = true - } + self.scrollNode = ASScrollNode() + + self.amountNode = WalletInfoBalanceNode(dateTimeFormat: presentationData.dateTimeFormat) self.feesNode = ImmediateTextNode() self.feesNode.textAlignment = .center self.feesNode.maximumNumberOfLines = 2 self.feesNode.lineSpacing = 0.35 + self.feesInfoIconNode = ASImageNode() + self.feesInfoIconNode.displaysAsynchronously = false + self.feesInfoIconNode.displayWithoutProcessing = true + self.feesInfoIconNode.image = UIImage(bundleImageName: "Wallet/InfoIcon") + self.feesButtonNode = ASButtonNode() self.commentBackgroundNode = ASImageNode() self.commentBackgroundNode.contentMode = .scaleToFill + self.commentBackgroundNode.isUserInteractionEnabled = true self.commentTextNode = ImmediateTextNode() self.commentTextNode.textAlignment = .natural self.commentTextNode.maximumNumberOfLines = 0 + self.commentTextNode.isUserInteractionEnabled = false + + self.commentSeparatorNode = ASDisplayNode() + self.commentSeparatorNode.backgroundColor = self.presentationData.theme.list.itemPlainSeparatorColor self.addressTextNode = ImmediateTextNode() self.addressTextNode.maximumNumberOfLines = 4 @@ -327,14 +380,18 @@ private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode { self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.addSubnode(self.scrollNode) + self.addSubnode(self.feesNode) + self.addSubnode(self.feesInfoIconNode) + self.addSubnode(self.feesButtonNode) + self.scrollNode.addSubnode(self.commentBackgroundNode) + self.scrollNode.addSubnode(self.commentTextNode) + self.addSubnode(self.navigationBackgroundNode) + self.navigationBackgroundNode.addSubnode(self.navigationSeparatorNode) self.addSubnode(self.titleNode) self.addSubnode(self.timeNode) self.addSubnode(self.amountNode) - self.addSubnode(self.iconNode) - self.addSubnode(self.feesNode) - self.addSubnode(self.feesButtonNode) - self.addSubnode(self.commentBackgroundNode) - self.addSubnode(self.commentTextNode) + self.addSubnode(self.commentSeparatorNode) self.addSubnode(self.addressTextNode) self.addSubnode(self.buttonNode) @@ -346,9 +403,8 @@ private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode { self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.Wallet_TransactionInfo_Title, font: titleFont, textColor: textColor) - self.timeNode.attributedText = NSAttributedString(string: stringForFullDate(timestamp: Int32(clamping: timestamp), strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat), font: subtitleFont, textColor: seccondaryTextColor) - + let amountString: String let amountColor: UIColor if transferredValue <= 0 { @@ -358,8 +414,8 @@ private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode { amountString = "\(formatBalanceText(transferredValue, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator))" amountColor = self.presentationData.theme.info.incomingFundsTitleColor } - self.amountNode.attributedText = amountAttributedString(amountString, integralFont: integralFont, fractionalFont: fractionalFont, color: amountColor) - + self.amountNode.balance = (amountString, amountColor) + var feesString: String = "" if case let .completed(transaction) = walletTransaction { if transaction.storageFee != 0 { @@ -372,16 +428,18 @@ private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode { feesString.append(formatBalanceText(transaction.otherFee, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) + " transaction fee") } - if !feesString.isEmpty { - feesString.append("(?)") - } + self.feesInfoIconNode.isHidden = feesString.isEmpty } self.feesNode.attributedText = NSAttributedString(string: feesString, font: subtitleFont, textColor: seccondaryTextColor) self.feesButtonNode.addTarget(self, action: #selector(feesPressed), forControlEvents: .touchUpInside) - self.commentBackgroundNode.image = messageBubbleImage(incoming: transferredValue > 0, fillColor: UIColor(rgb: 0xf1f1f5), strokeColor: UIColor(rgb: 0xf1f1f5)) - self.commentTextNode.attributedText = NSAttributedString(string: extractDescription(walletTransaction), font: Font.regular(17.0), textColor: .black) + var commentBackgroundColor = presentationData.theme.transaction.descriptionBackgroundColor + if commentBackgroundColor.distance(to: presentationData.theme.list.plainBackgroundColor) < 100 { + commentBackgroundColor = UIColor(rgb: 0xf1f1f4) + } + self.commentBackgroundNode.image = messageBubbleImage(incoming: transferredValue > 0, fillColor: commentBackgroundColor, strokeColor: presentationData.theme.transaction.descriptionBackgroundColor) + self.commentTextNode.attributedText = NSAttributedString(string: extractDescription(walletTransaction), font: Font.regular(17.0), textColor: presentationData.theme.transaction.descriptionTextColor) let address = extractAddress(walletTransaction) var singleAddress: String? @@ -391,7 +449,7 @@ private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode { if let address = singleAddress { self.addressTextNode.attributedText = NSAttributedString(string: formatAddress(address), font: addressFont, textColor: textColor, paragraphAlignment: .justified) - self.buttonNode.title = "Send Grams to This Address" + self.buttonNode.title = presentationData.strings.Wallet_TransactionInfo_SendGrams self.buttonNode.pressed = { [weak self] in self?.send?(address) @@ -399,41 +457,191 @@ private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode { } } + override func didLoad() { + super.didLoad() + + self.scrollNode.view.delegate = self + self.scrollNode.view.alwaysBounceVertical = true + self.scrollNode.view.showsVerticalScrollIndicator = false + + let commentGestureRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapCommentGesture(_:))) + commentGestureRecognizer.tapActionAtPoint = { [weak self] point in + return .waitForSingleTap + } + self.commentBackgroundNode.view.addGestureRecognizer(commentGestureRecognizer) + + let addressGestureRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapAddressGesture(_:))) + addressGestureRecognizer.tapActionAtPoint = { [weak self] point in + return .waitForSingleTap + } + self.addressTextNode.view.addGestureRecognizer(addressGestureRecognizer) + } + + @objc func tapLongTapOrDoubleTapCommentGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .longTap: + let description = extractDescription(self.walletTransaction) + if !description.isEmpty { + self.displayCopyContextMenu?(self, self.commentBackgroundNode.convert(self.commentBackgroundNode.bounds, to: self), description) + } + default: + break + } + } + default: + break + } + } + + @objc func tapLongTapOrDoubleTapAddressGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .longTap: + let address = extractAddress(self.walletTransaction) + var singleAddress: String? + if case let .list(list) = address, list.count == 1 { + singleAddress = list.first + } + + if let address = singleAddress { + self.displayCopyContextMenu?(self, self.addressTextNode.convert(self.addressTextNode.bounds, to: self), address) + } + default: + break + } + } + default: + break + } + } + @objc private func feesPressed() { self.displayFeesTooltip?(self.feesNode, self.feesNode.bounds) } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateTitle(transition: .immediate) + } + + private func updateTitle(transition: ContainedViewLayoutTransition) { + guard let (layout, navigationHeight) = self.validLayout else { + return + } + + let width = layout.size.width + let sideInset: CGFloat = 16.0 + + let minOffset = navigationHeight + let maxOffset: CGFloat = 200.0 + + let nominalFeesHeight: CGFloat = 42.0 + let minHeaderOffset = minOffset + let maxHeaderOffset = (minOffset + maxOffset) / 2.0 + let maxHeaderPositionOffset = maxOffset - nominalFeesHeight + + let offset: CGFloat = max(0.0, maxOffset - self.scrollNode.view.contentOffset.y) + let effectiveOffset = max(offset, navigationHeight) + + let minFeesOffset = maxOffset - nominalFeesHeight + let maxFeesOffset = maxOffset + let feesTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minFeesOffset) / (maxFeesOffset - minFeesOffset))) + let feesAlpha: CGFloat = feesTransition + transition.updateAlpha(node: self.feesNode, alpha: feesAlpha) + + let headerScaleTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minHeaderOffset) / (maxHeaderOffset - minHeaderOffset))) + let balanceHeight = self.amountNode.update(width: width, scaleTransition: headerScaleTransition, transition: transition) + let balanceSize = CGSize(width: width, height: balanceHeight) + + let maxHeaderScale: CGFloat = min(1.0, (width - 40.0) / balanceSize.width) + let minHeaderScale: CGFloat = min(0.435, (width - 80.0 * 2.0) / balanceSize.width) + + let minHeaderHeight: CGFloat = balanceSize.height - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { - var insets = layout.insets(options: []) - insets.top += navigationHeight - let inset: CGFloat = 22.0 + let minHeaderY = floor((navigationHeight - minHeaderHeight) / 2.0) + let maxHeaderY: CGFloat = 90.0 + let headerPositionTransition: CGFloat = min(1.0, max(0.0, (effectiveOffset - minHeaderOffset) / (maxHeaderPositionOffset - minHeaderOffset))) + let headerY = headerPositionTransition * maxHeaderY + (1.0 - headerPositionTransition) * minHeaderY + let headerScale = headerScaleTransition * maxHeaderScale + (1.0 - headerScaleTransition) * minHeaderScale - let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: 10.0), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) + let balanceFrame = CGRect(origin: CGPoint(x: 0.0, y: headerY), size: balanceSize) + transition.updateFrame(node: self.amountNode, frame: balanceFrame) + transition.updateSublayerTransformScale(node: self.amountNode, scale: headerScale) - let subtitleSize = self.timeNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let subtitleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) - transition.updateFrame(node: self.timeNode, frame: subtitleFrame) - - let amountSize = self.amountNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let amountFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - amountSize.width) / 2.0) + 18.0, y: 90.0), size: amountSize) - transition.updateFrame(node: self.amountNode, frame: amountFrame) - - let iconSize = CGSize(width: 50.0, height: 50.0) - let iconFrame = CGRect(origin: CGPoint(x: amountFrame.minX - iconSize.width, y: amountFrame.minY), size: iconSize) - self.iconNode.updateLayout(size: iconFrame.size) - self.iconNode.frame = iconFrame - - let feesSize = self.feesNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let feesFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - feesSize.width) / 2.0), y: amountFrame.maxY + 8.0), size: feesSize) + let feesSize = self.feesNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let feesFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - feesSize.width) / 2.0), y: headerY + 64.0), size: feesSize) transition.updateFrame(node: self.feesNode, frame: feesFrame) transition.updateFrame(node: self.feesButtonNode, frame: feesFrame) + self.feesButtonNode.isUserInteractionEnabled = feesAlpha > 1.0 - CGFloat.ulpOfOne + + let minTitleOffset = minOffset + let maxTitleOffset = (minOffset + maxOffset) / 2.0 + let titleTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minTitleOffset) / (maxTitleOffset - minTitleOffset))) + let titleAlpha: CGFloat = titleTransition * titleTransition + transition.updateAlpha(node: self.titleNode, alpha: titleAlpha) + transition.updateAlpha(node: self.timeNode, alpha: titleAlpha) + + let minTitleY: CGFloat = -44.0 + let maxTitleY: CGFloat = 10.0 + let titleY: CGFloat = titleTransition * maxTitleY + (1.0 - titleTransition) * minTitleY + let titleSize = self.titleNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: titleY), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + let subtitleSize = self.timeNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) + transition.updateFrame(node: self.timeNode, frame: subtitleFrame) + + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) + let navigationAlpha: CGFloat = (headerScaleTransition <= 0.0 + CGFloat.ulpOfOne) ? 1.0 : 0.0 + if self.navigationBackgroundNode.alpha != navigationAlpha { + alphaTransition.updateAlpha(node: self.navigationBackgroundNode, alpha: navigationAlpha, beginWithCurrentState: true) + } + + let separatorAlpha: CGFloat = self.scrollNode.view.contentOffset.y + self.scrollNode.frame.height >= self.scrollNode.view.contentSize.height ? 0.0 : 1.0 + if self.commentSeparatorNode.alpha != separatorAlpha { + alphaTransition.updateAlpha(node: self.commentSeparatorNode, alpha: separatorAlpha, beginWithCurrentState: true) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationHeight) + + var insets = layout.insets(options: []) + insets.top += navigationHeight + let sideInset: CGFloat = 22.0 + + self.updateTitle(transition: transition) + + let buttonSideInset: CGFloat = 16.0 + let bottomInset = insets.bottom + 10.0 + let buttonWidth = layout.size.width - buttonSideInset * 2.0 + let buttonHeight: CGFloat = 50.0 + let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight)) + transition.updateFrame(node: self.buttonNode, frame: buttonFrame) + self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) + + let addressSize = self.addressTextNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) + let addressFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - addressSize.width) / 2.0), y: buttonFrame.minY - addressSize.height - 34.0), size: addressSize) + transition.updateFrame(node: self.addressTextNode, frame: addressFrame) + + transition.updateFrame(node: self.navigationBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: navigationHeight)) + transition.updateFrame(node: self.navigationSeparatorNode, frame: CGRect(x: 0.0, y: navigationHeight, width: layout.size.width, height: UIScreenPixel)) + + let commentSeparatorFrame = CGRect(x: 0.0, y: addressFrame.minY - 36.0, width: layout.size.width, height: UIScreenPixel) + transition.updateFrame(node: self.commentSeparatorNode, frame: commentSeparatorFrame) + + let scrollFrame = CGRect(x: 0.0, y: navigationHeight, width: layout.size.width, height: commentSeparatorFrame.minY - navigationHeight) + transition.updateFrame(node: self.scrollNode, frame: scrollFrame) let commentSize = self.commentTextNode.updateLayout(CGSize(width: layout.size.width - 36.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) - let commentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - commentSize.width) / 2.0), y: amountFrame.maxY + 84.0), size: CGSize(width: commentSize.width, height: commentSize.height)) + let commentOrigin = CGPoint(x: floor((layout.size.width - commentSize.width) / 2.0), y: 175.0) + let commentFrame = CGRect(origin: commentOrigin, size: commentSize) transition.updateFrame(node: self.commentTextNode, frame: commentFrame) - + var commentBackgroundFrame = commentSize.width > 0.0 ? commentFrame.insetBy(dx: -11.0, dy: -7.0) : CGRect() commentBackgroundFrame.size.width += 7.0 if self.incoming { @@ -441,16 +649,12 @@ private final class WalletTransactionInfoScreenNode: ViewControllerTracingNode { } transition.updateFrame(node: self.commentBackgroundNode, frame: commentBackgroundFrame) - let buttonSideInset: CGFloat = 16.0 - let bottomInset = insets.bottom + 10.0 - let buttonWidth = layout.size.width - buttonSideInset * 2.0 - let buttonHeight: CGFloat = 50.0 + let contentHeight = commentOrigin.y + commentBackgroundFrame.height + self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: contentHeight) - let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight)) - transition.updateFrame(node: self.buttonNode, frame: buttonFrame) - self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition) - - let addressSize = self.addressTextNode.updateLayout(CGSize(width: layout.size.width - inset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.addressTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - addressSize.width) / 2.0), y: buttonFrame.minY - addressSize.height - 44.0), size: addressSize)) + let isScrollEnabled = contentHeight - scrollFrame.height > 20.0 + self.scrollNode.view.isScrollEnabled = isScrollEnabled + self.scrollNode.clipsToBounds = isScrollEnabled + self.commentSeparatorNode.isHidden = !isScrollEnabled } } diff --git a/submodules/WalletUI/Sources/WalletWordDisplayScreen.swift b/submodules/WalletUI/Sources/WalletWordDisplayScreen.swift index 2c986e5eb0..bbc72168fc 100644 --- a/submodules/WalletUI/Sources/WalletWordDisplayScreen.swift +++ b/submodules/WalletUI/Sources/WalletWordDisplayScreen.swift @@ -254,14 +254,16 @@ private final class WalletWordDisplayScreenNode: ViewControllerTracingNode, UISc } private func updateTitle() { - guard let listTitleFrame = self.listTitleFrame else { + guard let layout = self.validLayout, let listTitleFrame = self.listTitleFrame else { return } let scrollView = self.scrollNode.view let navigationHeight = self.navigationHeight ?? 0.0 - let minY = navigationHeight - 44.0 + floor(44.0 / 2.0) - let maxY = minY + 44.0 + let nominalNavigationHeight = navigationHeight - (layout.0.statusBarHeight ?? 0.0) + + let minY = navigationHeight - nominalNavigationHeight + floor(nominalNavigationHeight / 2.0) + let maxY = minY + nominalNavigationHeight let y = max(minY, -scrollView.contentOffset.y + listTitleFrame.midY) var t = (y - minY) / (maxY - minY) t = max(0.0, min(1.0, t)) diff --git a/submodules/ffmpeg/FFMpeg/build-ffmpeg.sh b/submodules/ffmpeg/FFMpeg/build-ffmpeg.sh index 5086bbe443..cc7f8a753b 100755 --- a/submodules/ffmpeg/FFMpeg/build-ffmpeg.sh +++ b/submodules/ffmpeg/FFMpeg/build-ffmpeg.sh @@ -55,7 +55,7 @@ CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \ --enable-libopus \ --enable-audiotoolbox \ --enable-bsf=aac_adtstoasc \ - --enable-decoder=h264,hevc,libopus,mp3_at,aac_at,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \ + --enable-decoder=h264,hevc,libopus,mp3_at,aac,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \ --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska \ --enable-parser=aac,h264,mp3,libopus \ --enable-protocol=file \