diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 472f4d278d..c1c9b89fcf 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 09874E5721078FA100E190B8 /* YoutubeUserScript.js in Resources */ = {isa = PBXBuildFile; fileRef = 0979788121065F8B0077D77F /* YoutubeUserScript.js */; }; 09874E582107A4C300E190B8 /* VimeoEmbedImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09874E3A21075BF400E190B8 /* VimeoEmbedImplementation.swift */; }; 09874E592107BD4100E190B8 /* GenericEmbedImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09874E4021075C1700E190B8 /* GenericEmbedImplementation.swift */; }; + 09C500242142BA6400EF253E /* ItemListWebsiteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */; }; D007019C2029E8F2006B9E34 /* LegqacyICloudFileController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007019B2029E8F2006B9E34 /* LegqacyICloudFileController.swift */; }; D007019E2029EFDD006B9E34 /* ICloudResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007019D2029EFDD006B9E34 /* ICloudResources.swift */; }; D00701A12029F6D0006B9E34 /* TGMimeTypeMap.h in Headers */ = {isa = PBXBuildFile; fileRef = D007019F2029F6D0006B9E34 /* TGMimeTypeMap.h */; }; @@ -1047,6 +1048,7 @@ 09874E4021075C1700E190B8 /* GenericEmbedImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericEmbedImplementation.swift; sourceTree = ""; }; 09874E4221075C3000E190B8 /* VKEmbedImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VKEmbedImplementation.swift; sourceTree = ""; }; 09874E4421075C3F00E190B8 /* StreamableEmbedImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamableEmbedImplementation.swift; sourceTree = ""; }; + 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListWebsiteItem.swift; sourceTree = ""; }; D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageContainingNode.swift; sourceTree = ""; }; D002A0D01E9B99F500A81812 /* SoftwareVideoSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoftwareVideoSource.swift; sourceTree = ""; }; D002A0D21E9BBE6700A81812 /* MultiplexedSoftwareVideoSourceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplexedSoftwareVideoSourceManager.swift; sourceTree = ""; }; @@ -4333,6 +4335,7 @@ D05A32DD1E6F0097002760B4 /* PrivacyAndSecurityController.swift */, D08984EF2114AE0C00918162 /* DataPrivacySettingsController.swift */, D05A32ED1E6F25A0002760B4 /* ItemListRecentSessionItem.swift */, + 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */, D05A32E91E6F143C002760B4 /* RecentSessionsController.swift */, D05A32EB1E6F1462002760B4 /* BlockedPeersController.swift */, D05B724C1E720393000BD3AD /* SelectivePrivacySettingsController.swift */, @@ -4822,6 +4825,7 @@ D0B4AF8B1EC1133600D51FF6 /* CallKitIntergation.swift in Sources */, D0FFF7F61F55B82500BEBC01 /* InstantPageAudioItem.swift in Sources */, D03AA4E7202DFB160056C405 /* ItemListEditableReorderControlNode.swift in Sources */, + 09C500242142BA6400EF253E /* ItemListWebsiteItem.swift in Sources */, D0EC6D0C1EB9F58800EBF1C3 /* stream.c in Sources */, D0EC6D0D1EB9F58800EBF1C3 /* MediaFrameSource.swift in Sources */, D0EC6D0E1EB9F58800EBF1C3 /* MediaPlaybackData.swift in Sources */, @@ -6041,7 +6045,6 @@ PRODUCT_NAME = TelegramUI; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; - SWIFT_COMPILATION_MODE = singlefile; SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 4.0; diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift index baa4137d1a..d7547e2193 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift @@ -236,6 +236,7 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { } override func dismiss(completion: (() -> Void)? = nil) { + self.navigationContentNode?.deactivate() self.controllerNode.animateOut(completion: { [weak self] in self?.presentingViewController?.dismiss(animated: true, completion: nil) }) diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift b/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift index 922a9d1dc9..170b9674c4 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionControllerNode.swift @@ -97,7 +97,7 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, var sections: [(String, [((String, String), String, Int)])] = [] for (names, id, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in - return lhs.0 < rhs.0 + return lhs.0.1 < rhs.0.1 }) { let title = String(names.1[names.1.startIndex ..< names.1.index(after: names.1.startIndex)]).uppercased() if sections.isEmpty || sections[sections.count - 1].0 != title { @@ -275,4 +275,8 @@ final class AuthorizationSequenceCountrySelectionControllerNode: ASDisplayNode, self.itemSelected(self.searchResults[indexPath.row]) } } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.view.endEditing(true) + } } diff --git a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift index 18afd282d4..f5a6680d9c 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift @@ -270,7 +270,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { var items: [AuthorizationLayoutItem] = [ AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), - AuthorizationLayoutItem(node: self.phoneAndCountryNode, size: CGSize(width: layout.size.width, height: 115.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 54.0, maxValue: 54.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), + AuthorizationLayoutItem(node: self.phoneAndCountryNode, size: CGSize(width: layout.size.width, height: 115.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 44.0, maxValue: 44.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), //AuthorizationLayoutItem(node: self.termsOfServiceNode, size: termsOfServiceSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 90.0, maxValue: 90.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), ] diff --git a/TelegramUI/BotCheckoutControllerNode.swift b/TelegramUI/BotCheckoutControllerNode.swift index a9f0ffe426..217715582e 100644 --- a/TelegramUI/BotCheckoutControllerNode.swift +++ b/TelegramUI/BotCheckoutControllerNode.swift @@ -732,7 +732,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, if value { strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) } else { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.Checkout_LiabilityAlertTitle, text: strongSelf.presentationData.strings.Checkout_LiabilityAlert(botPeer.displayTitle, providerPeer.displayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: strongSelf.presentationData.strings.Checkout_LiabilityAlertTitle, text: strongSelf.presentationData.strings.Checkout_LiabilityAlert(botPeer.displayTitle, providerPeer.displayTitle).0, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { if let strongSelf = self { let _ = ApplicationSpecificNotice.setBotPaymentLiability(postbox: strongSelf.account.postbox, peerId: strongSelf.messageId.peerId).start() strongSelf.pay(savedCredentialsToken: savedCredentialsToken, liabilityNoticeAccepted: true) diff --git a/TelegramUI/BotCheckoutInfoControllerNode.swift b/TelegramUI/BotCheckoutInfoControllerNode.swift index 12923594d9..1e9e365e33 100644 --- a/TelegramUI/BotCheckoutInfoControllerNode.swift +++ b/TelegramUI/BotCheckoutInfoControllerNode.swift @@ -165,7 +165,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi var sectionItems: [BotPaymentItemNode] = [] sectionItems.append(BotPaymentHeaderItemNode(text: strings.CheckoutInfo_ReceiverInfoTitle)) if invoice.requestedFields.contains(.name) { - let nameItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoName, placeholder: strings.CheckoutInfo_ReceiverInfoNamePlaceholder) + let nameItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoName, placeholder: strings.CheckoutInfo_ReceiverInfoNamePlaceholder, contentType: .name) nameItem.text = formInfo.name ?? "" self.nameItem = nameItem sectionItems.append(nameItem) @@ -173,7 +173,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, UIScrollVi self.nameItem = nil } if invoice.requestedFields.contains(.email) { - let emailItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoEmail, placeholder: strings.CheckoutInfo_ReceiverInfoEmailPlaceholder) + let emailItem = BotPaymentFieldItemNode(title: strings.CheckoutInfo_ReceiverInfoEmail, placeholder: strings.CheckoutInfo_ReceiverInfoEmailPlaceholder, contentType: .email) emailItem.text = formInfo.email ?? "" self.emailItem = emailItem sectionItems.append(emailItem) diff --git a/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift b/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift index e2a7a5ef20..9f7484ba15 100644 --- a/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift +++ b/TelegramUI/BotCheckoutNativeCardEntryControllerNode.swift @@ -98,7 +98,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode, sectionItems.append(BotPaymentHeaderItemNode(text: strings.Checkout_NewCard_CardholderNameTitle)) - let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .creditCardholderName) + let cardholderItem = BotPaymentFieldItemNode(title: "", placeholder: strings.Checkout_NewCard_CardholderNamePlaceholder, contentType: .name) self.cardholderItem = cardholderItem sectionItems.append(cardholderItem) diff --git a/TelegramUI/BotPaymentFieldItemNode.swift b/TelegramUI/BotPaymentFieldItemNode.swift index ab08e3d089..c365ffe9c4 100644 --- a/TelegramUI/BotPaymentFieldItemNode.swift +++ b/TelegramUI/BotPaymentFieldItemNode.swift @@ -6,7 +6,7 @@ private let titleFont = Font.regular(17.0) enum BotPaymentFieldContentType { case generic - case creditCardholderName + case name case phoneNumber case email case address @@ -47,7 +47,7 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate { switch contentType { case .generic: break - case .creditCardholderName, .address: + case .name, .address: self.textField.textField.autocorrectionType = .no case .phoneNumber: self.textField.textField.keyboardType = .numberPad @@ -129,7 +129,7 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if !string.isEmpty { - if case .creditCardholderName = self.contentType { + if case .name = self.contentType { if let lowerBound = textField.position(from: textField.beginningOfDocument, offset: range.lowerBound), let upperBound = textField.position(from: textField.beginningOfDocument, offset: range.upperBound), let fieldRange = textField.textRange(from: lowerBound, to: upperBound) { textField.replace(fieldRange, withText: string.uppercased()) self.editingChanged() diff --git a/TelegramUI/CallController.swift b/TelegramUI/CallController.swift index 085416285e..2769bbc41f 100644 --- a/TelegramUI/CallController.swift +++ b/TelegramUI/CallController.swift @@ -157,7 +157,7 @@ public final class CallController: ViewController { let _ = self?.dismiss() } - self.controllerNode.disissedInteractively = { [weak self] in + self.controllerNode.dismissedInteractively = { [weak self] in self?.animatedAppearance = false self?.presentingViewController?.dismiss(animated: false, completion: nil) } diff --git a/TelegramUI/CallControllerNode.swift b/TelegramUI/CallControllerNode.swift index 108a8fc14b..c54419f315 100644 --- a/TelegramUI/CallControllerNode.swift +++ b/TelegramUI/CallControllerNode.swift @@ -45,7 +45,7 @@ final class CallControllerNode: ASDisplayNode { var acceptCall: (() -> Void)? var endCall: (() -> Void)? var back: (() -> Void)? - var disissedInteractively: (() -> Void)? + var dismissedInteractively: (() -> Void)? init(account: Account, presentationData: PresentationData, statusBar: StatusBar) { self.account = account @@ -342,7 +342,7 @@ final class CallControllerNode: ASDisplayNode { if layout.size.height.isEqual(to: 480.0) { buttonsOffset = 53.0 } else { - buttonsOffset = 63.0 + buttonsOffset = 73.0 } } else { buttonsOffset = 83.0 @@ -352,7 +352,7 @@ final class CallControllerNode: ASDisplayNode { transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusOffset), size: CGSize(width: layout.size.width, height: statusHeight))) self.buttonsNode.updateLayout(constrainedWidth: layout.size.width, transition: transition) - transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - (buttonsOffset - 40.0) - buttonsHeight), size: CGSize(width: layout.size.width, height: buttonsHeight))) + transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.safeInsets.bottom), size: CGSize(width: layout.size.width, height: buttonsHeight))) let keyTextSize = self.keyButtonNode.frame.size transition.updateFrame(node: self.keyButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - keyTextSize.width - 8.0, y: navigationOffset + 8.0), size: keyTextSize)) @@ -418,7 +418,7 @@ final class CallControllerNode: ASDisplayNode { bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height) self.bounds = bounds self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseOut, completion: { [weak self] _ in - self?.disissedInteractively?() + self?.dismissedInteractively?() }) } case .cancelled: @@ -432,3 +432,17 @@ final class CallControllerNode: ASDisplayNode { } } } + +final private class CallControllerDebugNode: ASDisplayNode { + + private let disposable = MetaDisposable() + + override init() { + super.init() + + } + + deinit { + disposable.dispose() + } +} diff --git a/TelegramUI/CallKitIntergation.swift b/TelegramUI/CallKitIntergation.swift index ca55dec8b6..0c39209d60 100644 --- a/TelegramUI/CallKitIntergation.swift +++ b/TelegramUI/CallKitIntergation.swift @@ -7,16 +7,24 @@ import SwiftSignalKit final class CallKitIntegration { private let providerDelegate: AnyObject + public static var isAvailable: Bool { + if #available(iOSApplicationExtension 10.0, *) { + return Locale.current.regionCode?.lowercased() != "cn" + } else { + return false + } + } + private let audioSessionActivePromise = ValuePromise(false, ignoreRepeated: true) var audioSessionActive: Signal { return self.audioSessionActivePromise.get() } init?(startCall: @escaping (UUID, String) -> Signal, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal, audioSessionActivationChanged: @escaping (Bool) -> Void) { - if Locale.current.regionCode?.lowercased() == "cn" { + if !CallKitIntegration.isAvailable { return nil } - + #if (arch(i386) || arch(x86_64)) && os(iOS) return nil #else diff --git a/TelegramUI/ChatInfoTitlePanelNode.swift b/TelegramUI/ChatInfoTitlePanelNode.swift index 902b68d637..9a10a76621 100644 --- a/TelegramUI/ChatInfoTitlePanelNode.swift +++ b/TelegramUI/ChatInfoTitlePanelNode.swift @@ -73,9 +73,7 @@ private func peerButtons(_ peer: Peer, interfaceState: ChatPresentationInterface return buttons } else if let _ = peer as? TelegramSecretChat { var buttons: [ChatInfoTitleButton] = [.search, muteAction] - if interfaceState.callsAvailable { - buttons.append(.call) - } + buttons.append(.call) buttons.append(.info) return buttons } else if let channel = peer as? TelegramChannel { diff --git a/TelegramUI/ChatItemGalleryFooterContentNode.swift b/TelegramUI/ChatItemGalleryFooterContentNode.swift index acf0ffc64f..9a007a3298 100644 --- a/TelegramUI/ChatItemGalleryFooterContentNode.swift +++ b/TelegramUI/ChatItemGalleryFooterContentNode.swift @@ -517,6 +517,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { subject = .image(image.representations) } else if let webpage = m as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let _ = content.image { preferredAction = .saveToCameraRoll + } else if let file = m as? TelegramMediaFile, file.isAnimated { + preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Preview_SaveGif, action: { [weak self] in + if let strongSelf = self { + let message = messages[0] + let _ = addSavedGif(postbox: strongSelf.account.postbox, fileReference: .message(message: MessageReference(message), media: file)).start() + } + })) } } let shareController = ShareController(account: strongSelf.account, subject: subject, preferredAction: preferredAction) diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 44d82e5502..e646a7fb39 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -382,43 +382,37 @@ private func universalServiceMessageString(theme: PresentationTheme?, strings: P case let .customText(text, entities): attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, fixedFont: titleFont) case let .botDomainAccessGranted(domain): - attributedString = NSAttributedString(string: "Granted access to \(domain)", font: titleFont, textColor: primaryTextColor) + attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).0, font: titleFont, textColor: primaryTextColor) case let .botSentSecureValues(types): var typesString = "" + var hasIdentity = false + var hasAddress = false for type in types { if !typesString.isEmpty { typesString.append(", ") } switch type { case .personalDetails: - typesString.append("personal detail") - case .passport: - typesString.append("passport") - case .internalPassport: - typesString.append("internal passport") - case .driversLicense: - typesString.append("passport") - case .idCard: - typesString.append("ID card") + typesString.append(strings.Notification_PassportValuePersonalDetails) + case .passport, .internalPassport, .driversLicense, .idCard: + if !hasIdentity { + typesString.append(strings.Notification_PassportValueProofOfIdentity) + hasIdentity = true + } case .address: - typesString.append("residential address") - case .passportRegistration: - typesString.append("passport registration") - case .temporaryRegistration: - typesString.append("temporary registration") - case .bankStatement: - typesString.append("bank statement") - case .utilityBill: - typesString.append("utility bill") - case .rentalAgreement: - typesString.append("rental agreement") + typesString.append(strings.Notification_PassportValueAddress) + case .bankStatement, .utilityBill, .rentalAgreement, .passportRegistration, .temporaryRegistration: + if !hasAddress { + typesString.append(strings.Notification_PassportValueProofOfAddress) + hasAddress = true + } case .phone: - typesString.append("phone number") + typesString.append(strings.Notification_PassportValuePhone) case .email: - typesString.append("email address") + typesString.append(strings.Notification_PassportValueEmail) } } - attributedString = NSAttributedString(string: "Sent \(typesString)", font: titleFont, textColor: primaryTextColor) + attributedString = NSAttributedString(string: strings.Notification_PassportValuesSentMessage(message.author?.compactDisplayTitle ?? "", typesString).0, font: titleFont, textColor: primaryTextColor) case .unknown: attributedString = nil } @@ -564,7 +558,6 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) let layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) - if let _ = image { backgroundSize.height += imageSize.height + 10 } diff --git a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift index c6951ca6a2..0bf933cca8 100644 --- a/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift +++ b/TelegramUI/ChatMessageInvoiceBubbleContentNode.swift @@ -48,11 +48,13 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { var text: String? var mediaAndFlags: (Media, ChatMessageAttachedContentNodeMediaFlags)? + var automaticDownloadSettings = item.controllerInteraction.automaticMediaDownloadSettings if let invoice = invoice { title = invoice.title text = invoice.description if let image = invoice.photo { + automaticDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings mediaAndFlags = (image, [.preferMediaBeforeText]) } else { let invoiceLabel = item.presentationData.strings.Message_InvoiceLabel @@ -68,7 +70,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode { } } - let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, item.controllerInteraction.automaticMediaDownloadSettings, item.associatedData, item.account, item.controllerInteraction, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, nil, false, layoutConstants, constrainedSize) + let (initialWidth, continueLayout) = contentNodeLayout(item.presentationData, automaticDownloadSettings, item.associatedData, item.account, item.controllerInteraction, item.message, item.read, title, subtitle, text, nil, mediaAndFlags, nil, nil, false, layoutConstants, constrainedSize) let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 8.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index a8711e24d8..842c1616cc 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -376,7 +376,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { |> mapToQueue { update, chatPresentationData -> Signal in let processedView = chatRecentActionsEntries(entries: update.0, presentationData: chatPresentationData) let previous = previousView.swap(processedView) - + var prepareOnMainQueue = false if let previous = previous, previous == processedView { diff --git a/TelegramUI/DateSelectionActionSheetController.swift b/TelegramUI/DateSelectionActionSheetController.swift index 1981bc1ee2..916c65e53a 100644 --- a/TelegramUI/DateSelectionActionSheetController.swift +++ b/TelegramUI/DateSelectionActionSheetController.swift @@ -95,7 +95,7 @@ private final class DateSelectionActionSheetItemNode: ActionSheetItemNode { if let minimumDate = minimumDate { self.pickerView.minimumDate = minimumDate } - if let maximumDate = minimumDate { + if let maximumDate = maximumDate { self.pickerView.maximumDate = maximumDate } else { self.pickerView.maximumDate = Date(timeIntervalSince1970: Double(Int32.max - 1)) diff --git a/TelegramUI/HashtagSearchControllerNode.swift b/TelegramUI/HashtagSearchControllerNode.swift index ee39c24c7a..fa733e73e4 100644 --- a/TelegramUI/HashtagSearchControllerNode.swift +++ b/TelegramUI/HashtagSearchControllerNode.swift @@ -34,14 +34,14 @@ final class HashtagSearchControllerNode: ASDisplayNode { self.segmentedControl = UISegmentedControl(items: [peer?.displayTitle ?? "", strings.HashtagSearch_AllChats]) self.segmentedControl.tintColor = theme.rootController.navigationBar.accentTextColor - self.segmentedControl.selectedSegmentIndex = 1 + self.segmentedControl.selectedSegmentIndex = 0 if let peer = peer { self.chatController = ChatController(account: account, chatLocation: .peer(peer.id), messageId: nil, botStart: nil, mode: .inline) } else { self.chatController = nil } - + super.init() self.setViewBlock({ @@ -50,8 +50,8 @@ final class HashtagSearchControllerNode: ASDisplayNode { self.backgroundColor = theme.chatList.backgroundColor - self.listNode.isHidden = true self.addSubnode(self.listNode) + self.listNode.isHidden = true self.segmentedControl.addTarget(self, action: #selector(self.indexChanged), for: .valueChanged) } @@ -71,18 +71,7 @@ final class HashtagSearchControllerNode: ASDisplayNode { self.enqueuedTransitions.remove(at: 0) let options = ListViewDeleteAndInsertOptions() - - let displayingResults = transition.displayingResults - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in - if let strongSelf = self { - if displayingResults != !strongSelf.listNode.isHidden { - if strongSelf.listNode.isHidden { - strongSelf.listNode.isHidden = false - strongSelf.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - } - } - }) + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) } } @@ -120,7 +109,6 @@ final class HashtagSearchControllerNode: ASDisplayNode { chatController.viewWillAppear(false) self.insertSubnode(chatController.displayNode, at: 0) chatController.viewDidAppear(false) - chatController.displayNode.isHidden = true chatController.beginMessageSearch(self.query) } diff --git a/TelegramUI/ItemListControllerSegmentedTitleView.swift b/TelegramUI/ItemListControllerSegmentedTitleView.swift index c0aaad0c4a..f62f0667d9 100644 --- a/TelegramUI/ItemListControllerSegmentedTitleView.swift +++ b/TelegramUI/ItemListControllerSegmentedTitleView.swift @@ -57,7 +57,7 @@ final class ItemListControllerSegmentedTitleView: UIView { let size = self.bounds.size var controlSize = self.control.sizeThatFits(size) - controlSize.width = min(size.width, max(200.0, controlSize.width)) + controlSize.width = min(size.width, max(160.0, controlSize.width)) self.control.frame = CGRect(origin: CGPoint(x: floor((size.width - controlSize.width) / 2.0), y: floor((size.height - controlSize.height) / 2.0)), size: controlSize) } diff --git a/TelegramUI/ItemListWebsiteItem.swift b/TelegramUI/ItemListWebsiteItem.swift new file mode 100644 index 0000000000..bdc836901a --- /dev/null +++ b/TelegramUI/ItemListWebsiteItem.swift @@ -0,0 +1,401 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +struct ItemListWebsiteItemEditing: Equatable { + let editing: Bool + let revealed: Bool + + static func ==(lhs: ItemListWebsiteItemEditing, rhs: ItemListWebsiteItemEditing) -> Bool { + if lhs.editing != rhs.editing { + return false + } + if lhs.revealed != rhs.revealed { + return false + } + return true + } +} + +final class ItemListWebsiteItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings + let website: WebAuthorization + let peer: Peer? + let enabled: Bool + let editing: Bool + let revealed: Bool + let sectionId: ItemListSectionId + let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void + let removeSession: (Int64) -> Void + + init(theme: PresentationTheme, strings: PresentationStrings, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void) { + self.theme = theme + self.strings = strings + self.website = website + self.peer = peer + self.enabled = enabled + self.editing = editing + self.revealed = revealed + self.sectionId = sectionId + self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions + self.removeSession = removeSession + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = ItemListWebsiteItemNode() + 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 + + completion(node, { + return (nil, { apply(false) }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? ItemListWebsiteItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + var animated = true + if case .None = animation { + animated = false + } + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply(animated) + }) + } + } + } + } + } +} + +private let titleFont = Font.medium(15.0) +private let textFont = Font.regular(13.0) + +class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private var disabledOverlayNode: ASDisplayNode? + + private let titleNode: TextNode + private let appNode: TextNode + private let locationNode: TextNode + private let labelNode: TextNode + + private var layoutParams: (ItemListWebsiteItem, ListViewItemLayoutParams, ItemListNeighbors)? + + private var editableControlNode: ItemListEditableControlNode? + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.appNode = TextNode() + self.appNode.isLayerBacked = true + self.appNode.contentMode = .left + self.appNode.contentsScale = UIScreen.main.scale + + self.locationNode = TextNode() + self.locationNode.isLayerBacked = true + self.locationNode.contentMode = .left + self.locationNode.contentsScale = UIScreen.main.scale + + self.labelNode = TextNode() + self.labelNode.isLayerBacked = true + self.labelNode.contentMode = .left + self.labelNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.appNode) + self.addSubnode(self.locationNode) + self.addSubnode(self.labelNode) + } + + func asyncLayout() -> (_ item: ItemListWebsiteItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeAppLayout = TextNode.asyncLayout(self.appNode) + let makeLocationLayout = TextNode.asyncLayout(self.locationNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + + var currentDisabledOverlayNode = self.disabledOverlayNode + + let currentItem = self.layoutParams?.0 + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + + var titleAttributedString: NSAttributedString? + var appAttributedString: NSAttributedString? + var locationAttributedString: NSAttributedString? + var labelAttributedString: NSAttributedString? + + let peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.AuthSessions_LogOut, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] + + let rightInset: CGFloat = params.rightInset + + if let user = item.peer as? TelegramUser { + titleAttributedString = NSAttributedString(string: user.displayTitle, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) + } + + var appString = "" + if !item.website.domain.isEmpty { + appString = item.website.domain + } + + if !item.website.browser.isEmpty { + appString = item.website.browser + } + + if !item.website.platform.isEmpty { + if !appString.isEmpty { + appString += ", " + } + appString += item.website.platform + } + + appAttributedString = NSAttributedString(string: appString, font: textFont, textColor: item.theme.list.itemPrimaryTextColor) + locationAttributedString = NSAttributedString(string: "\(item.website.ip) — \(item.website.region)", font: textFont, textColor: item.theme.list.itemSecondaryTextColor) + + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.website.dateActive, relativeTo: timestamp, timeFormat: .regular) + labelAttributedString = NSAttributedString(string: dateText, font: textFont, textColor: item.theme.list.itemSecondaryTextColor) + + let leftInset: CGFloat = 15.0 + params.leftInset + + var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + + let editingOffset: CGFloat + if item.editing { + let sizeAndApply = editableControlLayout(75.0, item.theme, false) + editableControlSizeAndApply = sizeAndApply + editingOffset = sizeAndApply.0.width + } else { + editingOffset = 0.0 + } + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (appLayout, appApply) = makeAppLayout(TextNodeLayoutArguments(attributedString: appAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (locationLayout, locationApply) = makeLocationLayout(TextNodeLayoutArguments(attributedString: locationAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let insets = itemListNeighborsGroupedInsets(neighbors) + let contentSize = CGSize(width: params.width, height: 75.0) + let separatorHeight = UIScreenPixel + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + if !item.enabled { + if currentDisabledOverlayNode == nil { + currentDisabledOverlayNode = ASDisplayNode() + currentDisabledOverlayNode?.backgroundColor = UIColor(white: 1.0, alpha: 0.5) + } + } else { + currentDisabledOverlayNode = nil + } + + return (layout, { [weak self] animated in + if let strongSelf = self { + strongSelf.layoutParams = (item, params, neighbors) + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + + let revealOffset = strongSelf.revealOffset + + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + if let currentDisabledOverlayNode = currentDisabledOverlayNode { + if currentDisabledOverlayNode != strongSelf.disabledOverlayNode { + strongSelf.disabledOverlayNode = currentDisabledOverlayNode + strongSelf.addSubnode(currentDisabledOverlayNode) + currentDisabledOverlayNode.alpha = 0.0 + transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0) + currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)) + } else { + transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))) + } + } else if let disabledOverlayNode = strongSelf.disabledOverlayNode { + transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in + disabledOverlayNode?.removeFromSupernode() + }) + strongSelf.disabledOverlayNode = nil + } + + if let editableControlSizeAndApply = editableControlSizeAndApply { + if strongSelf.editableControlNode == nil { + let editableControlNode = editableControlSizeAndApply.1() + editableControlNode.tapped = { + if let strongSelf = self { + strongSelf.setRevealOptionsOpened(true, animated: true) + strongSelf.revealOptionsInteractivelyOpened() + } + } + strongSelf.editableControlNode = editableControlNode + strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + editableControlNode.frame = editableControlFrame + transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) + editableControlNode.alpha = 0.0 + transition.updateAlpha(node: editableControlNode, alpha: 1.0) + } + strongSelf.editableControlNode?.isHidden = false + } else if let editableControlNode = strongSelf.editableControlNode { + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = -editableControlFrame.size.width + strongSelf.editableControlNode = nil + transition.updateAlpha(node: editableControlNode, alpha: 0.0) + transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in + editableControlNode?.removeFromSupernode() + }) + } + + let _ = labelApply() + let _ = titleApply() + let _ = appApply() + let _ = locationApply() + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + editingOffset + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + } + 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))) + transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) + transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) + + transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - 15.0 - rightInset, y: 10.0), size: labelLayout.size)) + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 10.0), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 30.0), size: appLayout.size)) + transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 50.0), size: locationLayout.size)) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 75.0 + UIScreenPixel + UIScreenPixel)) + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) + strongSelf.setRevealOptionsOpened(item.revealed, animated: animated) + } + }) + } + } + + 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) + } + + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + guard let params = self.layoutParams?.1 else { + return + } + + let leftInset: CGFloat = 15.0 + params.leftInset + + let editingOffset: CGFloat + if let editableControlNode = self.editableControlNode { + editingOffset = editableControlNode.bounds.size.width + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = params.leftInset + offset + transition.updateFrame(node: editableControlNode, frame: editableControlFrame) + } else { + editingOffset = 0.0 + } + + transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - self.labelNode.bounds.size.width - 15.0, y: self.labelNode.frame.minY), size: self.labelNode.bounds.size)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) + transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.appNode.frame.minY), size: self.appNode.bounds.size)) + transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.locationNode.frame.minY), size: self.locationNode.bounds.size)) + } + + override func revealOptionsInteractivelyOpened() { + if let (item, _, _) = self.layoutParams { + item.setSessionIdWithRevealedOptions(item.website.hash, nil) + } + } + + override func revealOptionsInteractivelyClosed() { + if let (item, _, _) = self.layoutParams { + item.setSessionIdWithRevealedOptions(nil, item.website.hash) + } + } + + override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + + if let (item, _, _) = self.layoutParams { + item.removeSession(item.website.hash) + } + } +} diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index 7434334914..e33fb8e551 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -15,9 +15,13 @@ func legacyAttachmentMenu(account: Account, peer: Peer, editMediaOptions: Messag var itemViews: [Any] = [] + var editing = false var canSendImageOrVideo = false + var canEditCurrent = false if let editMediaOptions = editMediaOptions, editMediaOptions.contains(.imageOrVideo) { canSendImageOrVideo = true + editing = true + canEditCurrent = true } else { canSendImageOrVideo = true } @@ -54,7 +58,7 @@ func legacyAttachmentMenu(account: Account, peer: Peer, editMediaOptions: Messag carouselItem.allowCaptions = true itemViews.append(carouselItem) - let galleryItem = TGMenuSheetButtonItemView(title: strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + let galleryItem = TGMenuSheetButtonItemView(title: editing ? strings.Conversation_EditingMessageMediaChange : strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in controller?.dismiss(animated: true) openGallery() })! @@ -63,13 +67,7 @@ func legacyAttachmentMenu(account: Account, peer: Peer, editMediaOptions: Messag underlyingViews.append(galleryItem) } - var canSendFiles = false - if let editMediaOptions = editMediaOptions, editMediaOptions.contains(.file) { - canSendFiles = true - } else { - canSendFiles = true - } - if canSendFiles { + if !editing { let fileItem = TGMenuSheetButtonItemView(title: strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, action: {[weak controller] in controller?.dismiss(animated: true) openFileGallery() @@ -78,6 +76,14 @@ func legacyAttachmentMenu(account: Account, peer: Peer, editMediaOptions: Messag underlyingViews.append(fileItem) } + if canEditCurrent { + let fileItem = TGMenuSheetButtonItemView(title: strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, action: {[weak controller] in + controller?.dismiss(animated: true) + openFileGallery() + })! + itemViews.append(fileItem) + } + if editMediaOptions == nil { let locationItem = TGMenuSheetButtonItemView(title: strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in controller?.dismiss(animated: true) diff --git a/TelegramUI/NetworkStatusTitleView.swift b/TelegramUI/NetworkStatusTitleView.swift index 05c64f6dbc..885ab77f5a 100644 --- a/TelegramUI/NetworkStatusTitleView.swift +++ b/TelegramUI/NetworkStatusTitleView.swift @@ -240,7 +240,9 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.proxyButton.isHidden { - return self.proxyButton.hitTest(point.offsetBy(dx: -self.proxyButton.frame.minX, dy: -self.proxyButton.frame.minY), with: event) + if let result = self.proxyButton.hitTest(point.offsetBy(dx: -self.proxyButton.frame.minX, dy: -self.proxyButton.frame.minY), with: event) { + return result; + } } return super.hitTest(point, with: event) } diff --git a/TelegramUI/OngoingCallContext.swift b/TelegramUI/OngoingCallContext.swift index ff8dee5952..07c5675108 100644 --- a/TelegramUI/OngoingCallContext.swift +++ b/TelegramUI/OngoingCallContext.swift @@ -92,7 +92,7 @@ final class OngoingCallContext { private let audioSessionDisposable = MetaDisposable() private var networkTypeDisposable: Disposable? - init(callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal) { + init(callSessionManager: CallSessionManager, internalId: CallSessionInternalId, allowP2P: Bool, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal) { let _ = setupLogs self.internalId = internalId @@ -109,7 +109,7 @@ final class OngoingCallContext { break } } - let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForType(initialNetworkType)) + let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), allowP2P: allowP2P, proxy: voipProxyServer, networkType: ongoingNetworkTypeForType(initialNetworkType)) self.contextRef = Unmanaged.passRetained(context) context.stateChanged = { [weak self] state in self?.contextState.set(.single(state)) diff --git a/TelegramUI/OngoingCallThreadLocalContext.h b/TelegramUI/OngoingCallThreadLocalContext.h index c88b33e184..b8303500e4 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.h +++ b/TelegramUI/OngoingCallThreadLocalContext.h @@ -53,7 +53,7 @@ typedef NS_ENUM(int32_t, OngoingCallNetworkType) { @property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallState); -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType; +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue allowP2P:(BOOL)allowP2P proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType; - (void)startWithKey:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer; - (void)stop; diff --git a/TelegramUI/OngoingCallThreadLocalContext.mm b/TelegramUI/OngoingCallThreadLocalContext.mm index 10e4851baa..f46c8e19a9 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.mm +++ b/TelegramUI/OngoingCallThreadLocalContext.mm @@ -181,7 +181,7 @@ static int callControllerNetworkTypeForType(OngoingCallNetworkType type) { TGVoipLoggingFunction = loggingFunction; } -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType { +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue allowP2P:(BOOL)allowP2P proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType { self = [super init]; if (self != nil) { _queue = queue; @@ -193,7 +193,7 @@ static int callControllerNetworkTypeForType(OngoingCallNetworkType type) { _callConnectTimeout = 30.0; _callPacketTimeout = 10.0; _dataSavingMode = 0; - _allowP2P = true; + _allowP2P = allowP2P; _networkType = networkType; _controller = new tgvoip::VoIPController(); @@ -263,7 +263,7 @@ static int callControllerNetworkTypeForType(OngoingCallNetworkType type) { _controller->SetEncryptionKey((char *)key.bytes, isOutgoing); /*releasable*/ - _controller->SetRemoteEndpoints(endpoints, _allowP2P, 65); + _controller->SetRemoteEndpoints(endpoints, _allowP2P, maxLayer); _controller->Start(); _controller->Connect(); diff --git a/TelegramUI/OpenInActionSheetController.swift b/TelegramUI/OpenInActionSheetController.swift index 5ebc7ca345..728b3c18b9 100644 --- a/TelegramUI/OpenInActionSheetController.swift +++ b/TelegramUI/OpenInActionSheetController.swift @@ -164,6 +164,13 @@ private final class OpenInActionSheetItemNode: ActionSheetItemNode { node.frame = CGRect(origin: CGPoint(x: nodeOffset, y: 0.0), size: nodeSize) nodeOffset += nodeSize.width } + + if let lastNode = self.openInNodes.last { + let contentSize = CGSize(width: lastNode.frame.maxX + nodeInset, height: self.scrollNode.frame.height) + if self.scrollNode.view.contentSize != contentSize { + self.scrollNode.view.contentSize = contentSize + } + } } } diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index 694ae67f08..0b3c932cb5 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -280,11 +280,16 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let recognizer = gestureRecognizer as? UIPanGestureRecognizer { let location = recognizer.location(in: self.view) - /*if let view = super.hitTest(location, with: nil) { - if view != self.view && view.gestureRecognizers != nil { - return false + if let view = super.hitTest(location, with: nil) { + if let gestureRecognizers = view.gestureRecognizers, view != self.view { + for gestureRecognizer in gestureRecognizers { + if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer, gestureRecognizer.isEnabled { + panGestureRecognizer.isEnabled = false + panGestureRecognizer.isEnabled = true + } + } } - }*/ + } } return true } diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index dd04e9e98b..45f5b53f62 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -2480,13 +2480,18 @@ private func drawOpenInAppIconBorder(into c: CGContext, arguments: TransformImag c.setStrokeColor(UIColor(rgb: 0xeeeeee).cgColor) c.setLineWidth(1.0) - var cornerRadius: CGFloat = 0.0 - if case let .Corner(radius) = arguments.corners.topLeft, radius > CGFloat.ulpOfOne { - cornerRadius = radius + var radius: CGFloat = 0.0 + if case let .Corner(cornerRadius) = arguments.corners.topLeft, cornerRadius > CGFloat.ulpOfOne { + radius = max(0, cornerRadius - 0.5) } - let path = UIBezierPath(roundedRect: arguments.drawingRect.insetBy(dx: 0.5, dy: 0.5), cornerRadius: cornerRadius) - c.addPath(path.cgPath) + let rect = arguments.drawingRect.insetBy(dx: 0.5, dy: 0.5) + c.move(to: CGPoint(x: rect.minX, y: rect.midY)) + c.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.midX, y: rect.minY), radius: radius) + c.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.midY), radius: radius) + c.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.midX, y: rect.maxY), radius: radius) + c.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY), tangent2End: CGPoint(x: rect.minX, y: rect.midY), radius: radius) + c.closePath() c.strokePath() } diff --git a/TelegramUI/PresentationCall.swift b/TelegramUI/PresentationCall.swift index bc23e45dfc..fd1391da89 100644 --- a/TelegramUI/PresentationCall.swift +++ b/TelegramUI/PresentationCall.swift @@ -184,8 +184,8 @@ public final class PresentationCall { private var sessionState: CallSession? private var callContextState: OngoingCallContextState? - private var ongoingGontext: OngoingCallContext - private var ongoingGontextStateDisposable: Disposable? + private var ongoingContext: OngoingCallContext + private var ongoingContextStateDisposable: Disposable? private var reportedIncomingCall = false private var sessionStateDisposable: Disposable? @@ -231,7 +231,7 @@ public final class PresentationCall { private var droppedCall = false private var dropCallKitCallTimer: SwiftSignalKit.Timer? - init(audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, proxyServer: ProxyServerSettings?, currentNetworkType: NetworkType, updatedNetworkType: Signal) { + init(audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, allowP2P: Bool, proxyServer: ProxyServerSettings?, currentNetworkType: NetworkType, updatedNetworkType: Signal) { self.audioSession = audioSession self.callSessionManager = callSessionManager self.callKitIntegration = callKitIntegration @@ -241,7 +241,7 @@ public final class PresentationCall { self.isOutgoing = isOutgoing self.peer = peer - self.ongoingGontext = OngoingCallContext(callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: currentNetworkType, updatedNetworkType: updatedNetworkType) + self.ongoingContext = OngoingCallContext(callSessionManager: self.callSessionManager, internalId: self.internalId, allowP2P: allowP2P, proxyServer: proxyServer, initialNetworkType: currentNetworkType, updatedNetworkType: updatedNetworkType) var didReceiveAudioOutputs = false self.sessionStateDisposable = (callSessionManager.callState(internalId: internalId) @@ -251,7 +251,7 @@ public final class PresentationCall { } }) - self.ongoingGontextStateDisposable = (self.ongoingGontext.state + self.ongoingContextStateDisposable = (self.ongoingContext.state |> deliverOnMainQueue).start(next: { [weak self] contextState in if let strongSelf = self { if let sessionState = strongSelf.sessionState { @@ -350,7 +350,7 @@ public final class PresentationCall { self.audioSessionShouldBeActiveDisposable?.dispose() self.audioSessionActiveDisposable?.dispose() self.sessionStateDisposable?.dispose() - self.ongoingGontextStateDisposable?.dispose() + self.ongoingContextStateDisposable?.dispose() self.audioSessionDisposable?.dispose() if let dropCallKitCallTimer = self.dropCallKitCallTimer { @@ -453,7 +453,7 @@ public final class PresentationCall { case let .active(key, _, connections, maxLayer): self.audioSessionShouldBeActive.set(true) if let _ = audioSessionControl, !wasActive || previousControl == nil { - self.ongoingGontext.start(key: key, isOutgoing: sessionState.isOutgoing, connections: connections, maxLayer: maxLayer, audioSessionActive: self.audioSessionActive.get()) + self.ongoingContext.start(key: key, isOutgoing: sessionState.isOutgoing, connections: connections, maxLayer: maxLayer, audioSessionActive: self.audioSessionActive.get()) if sessionState.isOutgoing { self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date()) } @@ -461,12 +461,12 @@ public final class PresentationCall { case .terminated: self.audioSessionShouldBeActive.set(true) if wasActive { - self.ongoingGontext.stop() + self.ongoingContext.stop() } default: self.audioSessionShouldBeActive.set(false) if wasActive { - self.ongoingGontext.stop() + self.ongoingContext.stop() } } if case .terminated = sessionState.state, !wasTerminated { @@ -548,20 +548,20 @@ public final class PresentationCall { func hangUp() -> Signal { self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp) - self.ongoingGontext.stop() + self.ongoingContext.stop() return self.hungUpPromise.get() } func rejectBusy() { self.callSessionManager.drop(internalId: self.internalId, reason: .busy) - self.ongoingGontext.stop() + self.ongoingContext.stop() } func toggleIsMuted() { self.isMutedValue = !self.isMutedValue self.isMutedPromise.set(self.isMutedValue) - self.ongoingGontext.setIsMuted(self.isMutedValue) + self.ongoingContext.setIsMuted(self.isMutedValue) } func setCurrentAudioOutput(_ output: AudioSessionOutput) { diff --git a/TelegramUI/PresentationCallManager.swift b/TelegramUI/PresentationCallManager.swift index b32ded80ca..83fed551e3 100644 --- a/TelegramUI/PresentationCallManager.swift +++ b/TelegramUI/PresentationCallManager.swift @@ -3,6 +3,21 @@ import Postbox import TelegramCore import SwiftSignalKit +private func p2pAllowed(settings: VoiceCallSettings?, isContact: Bool) -> Bool { + let mode = settings?.p2pMode ?? .contacts + switch (mode, isContact) { + case (.always, _), (.contacts, true): + return true + default: + return false + } +} + +private func callKitIntegrationIfEnabled(_ integration: CallKitIntegration?, settings: VoiceCallSettings?) -> CallKitIntegration? { + let enabled = settings?.enableSystemIntegration ?? true + return enabled ? integration : nil +} + private enum CurrentCall { case none case incomingRinging(CallSessionRingingState) @@ -52,6 +67,9 @@ public final class PresentationCallManager { private var proxyServer: ProxyServerSettings? private var proxyServerDisposable: Disposable? + private var callSettings: VoiceCallSettings? + private var callSettingsDisposable: Disposable? + public init(postbox: Postbox, networkType: Signal, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager) { self.postbox = postbox self.networkType = networkType @@ -82,25 +100,25 @@ public final class PresentationCallManager { }) self.ringingStatesDisposable = (callSessionManager.ringingStates() - |> mapToSignal { ringingStates -> Signal<[(Peer, CallSessionRingingState)], NoError> in + |> mapToSignal { ringingStates -> Signal<[(Peer, CallSessionRingingState, Bool)], NoError> in if ringingStates.isEmpty { return .single([]) } else { - return postbox.transaction { transaction -> [(Peer, CallSessionRingingState)] in - var result: [(Peer, CallSessionRingingState)] = [] + return postbox.transaction { transaction -> [(Peer, CallSessionRingingState, Bool)] in + var result: [(Peer, CallSessionRingingState, Bool)] = [] for state in ringingStates { if let peer = transaction.getPeer(state.peerId) { - result.append((peer, state)) + result.append((peer, state, transaction.isPeerContact(peerId: state.peerId))) } } return result } } } - |> mapToSignal { states -> Signal<([(Peer, CallSessionRingingState)], NetworkType), NoError> in + |> mapToSignal { states -> Signal<([(Peer, CallSessionRingingState, Bool)], NetworkType), NoError> in return networkType |> take(1) - |> map { currentNetworkType -> ([(Peer, CallSessionRingingState)], NetworkType) in + |> map { currentNetworkType -> ([(Peer, CallSessionRingingState, Bool)], NetworkType) in return (states, currentNetworkType) } } @@ -152,6 +170,13 @@ public final class PresentationCallManager { } } }) + + self.callSettingsDisposable = (postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.voiceCallSettings]) + |> deliverOnMainQueue).start(next: { [weak self] preferences in + if let strongSelf = self, let settings = preferences.values[ApplicationSpecificPreferencesKeys.voiceCallSettings] as? VoiceCallSettings { + strongSelf.callSettings = settings + } + }) } deinit { @@ -159,12 +184,13 @@ public final class PresentationCallManager { self.removeCurrentCallDisposable.dispose() self.startCallDisposable.dispose() self.proxyServerDisposable?.dispose() + self.callSettingsDisposable?.dispose() } - private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState)], currentNetworkType: NetworkType) { + private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState, Bool)], currentNetworkType: NetworkType) { if let firstState = ringingStates.first { if self.currentCall == nil { - let call = PresentationCall(audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: self.callKitIntegration, internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0, proxyServer: self.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: self.networkType) + let call = PresentationCall(audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(self.callKitIntegration, settings: self.callSettings), internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0, allowP2P: p2pAllowed(settings: self.callSettings, isContact: firstState.2), proxyServer: self.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: self.networkType) self.currentCall = call self.currentCallPromise.set(.single(call)) self.hasActiveCallsPromise.set(true) @@ -201,14 +227,17 @@ public final class PresentationCallManager { } private func startCall(peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal { - return (combineLatest(self.callSessionManager.request(peerId: peerId, internalId: internalId), self.networkType |> take(1)) + return (combineLatest(self.callSessionManager.request(peerId: peerId, internalId: internalId), self.networkType |> take(1), postbox.peerView(id: peerId) |> take(1) |> map({ peerView -> Bool in + return peerView.peerIsContact + })) |> deliverOnMainQueue - |> beforeNext { [weak self] internalId, currentNetworkType in + |> beforeNext { [weak self] internalId, currentNetworkType, isContact in if let strongSelf = self { if let currentCall = strongSelf.currentCall { currentCall.rejectBusy() } - let call = PresentationCall(audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: strongSelf.callKitIntegration, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, proxyServer: strongSelf.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: strongSelf.networkType) + + let call = PresentationCall(audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: callKitIntegrationIfEnabled(strongSelf.callKitIntegration, settings: strongSelf.callSettings), internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, allowP2P: p2pAllowed(settings: strongSelf.callSettings, isContact: isContact), proxyServer: strongSelf.proxyServer, currentNetworkType: currentNetworkType, updatedNetworkType: strongSelf.networkType) strongSelf.currentCall = call strongSelf.currentCallPromise.set(.single(call)) strongSelf.hasActiveCallsPromise.set(true) diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index a27bce62d0..063b7b5c36 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -368,7 +368,7 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign |> deliverOnMainQueue currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in if let info = info { - pushControllerImpl?(selectivePrivacySettingsController(account: account, kind: .presence, current: info.presence, updated: { updated in + pushControllerImpl?(selectivePrivacySettingsController(account: account, kind: .presence, current: info.presence, updated: { updated, _ in if let currentInfoDisposable = currentInfoDisposable { let applySetting: Signal = privacySettingsPromise.get() |> filter { $0 != nil } @@ -391,7 +391,7 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign |> deliverOnMainQueue currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in if let info = info { - pushControllerImpl?(selectivePrivacySettingsController(account: account, kind: .groupInvitations, current: info.groupInvitations, updated: { updated in + pushControllerImpl?(selectivePrivacySettingsController(account: account, kind: .groupInvitations, current: info.groupInvitations, updated: { updated, _ in if let currentInfoDisposable = currentInfoDisposable { let applySetting: Signal = privacySettingsPromise.get() |> filter { $0 != nil } @@ -409,13 +409,30 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign } })) }, openVoiceCallPrivacy: { - let signal = privacySettingsPromise.get() + let privacySignal = privacySettingsPromise.get() |> take(1) - |> deliverOnMainQueue - currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in + + let callsSignal = account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.voiceCallSettings]) + |> take(1) + |> map { view -> VoiceCallSettings in + let voiceCallSettings: VoiceCallSettings + if let value = view.values[ApplicationSpecificPreferencesKeys.voiceCallSettings] as? VoiceCallSettings { + voiceCallSettings = value + } else { + voiceCallSettings = VoiceCallSettings.defaultSettings + } + + return voiceCallSettings + } + + currentInfoDisposable.set((combineLatest(privacySignal, callsSignal) |> deliverOnMainQueue).start(next: { [weak currentInfoDisposable] info, callSettings in if let info = info { - pushControllerImpl?(selectivePrivacySettingsController(account: account, kind: .voiceCalls, current: info.voiceCalls, updated: { updated in - if let currentInfoDisposable = currentInfoDisposable { + pushControllerImpl?(selectivePrivacySettingsController(account: account, kind: .voiceCalls, current: info.voiceCalls, callSettings: callSettings, callIntegrationAvailable: CallKitIntegration.isAvailable, updated: { updated, updatedCallSettings in + if let currentInfoDisposable = currentInfoDisposable, let updatedCallSettings = updatedCallSettings { + let _ = updateVoiceCallSettingsSettingsInteractively(postbox: account.postbox, { _ in + return updatedCallSettings + }).start() + let applySetting: Signal = privacySettingsPromise.get() |> filter { $0 != nil } |> take(1) diff --git a/TelegramUI/RecentSessionsController.swift b/TelegramUI/RecentSessionsController.swift index 04d0a4e4d4..ece704095e 100644 --- a/TelegramUI/RecentSessionsController.swift +++ b/TelegramUI/RecentSessionsController.swift @@ -11,14 +11,25 @@ private final class RecentSessionsControllerArguments { let removeSession: (Int64) -> Void let terminateOtherSessions: () -> Void - init(account: Account, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void) { + let removeWebSession: (Int64) -> Void + let terminateAllWebSessions: () -> Void + + init(account: Account, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void) { self.account = account self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions self.removeSession = removeSession self.terminateOtherSessions = terminateOtherSessions + + self.removeWebSession = removeWebSession + self.terminateAllWebSessions = terminateAllWebSessions } } +private enum RecentSessionsMode: Int { + case sessions + case websites +} + private enum RecentSessionsSection: Int32 { case currentSession case otherSessions @@ -59,16 +70,18 @@ private enum RecentSessionsEntry: ItemListNodeEntry { case currentSessionHeader(PresentationTheme, String) case currentSession(PresentationTheme, PresentationStrings, RecentAccountSession) case terminateOtherSessions(PresentationTheme, String) + case terminateAllWebSessions(PresentationTheme, String) case currentSessionInfo(PresentationTheme, String) case otherSessionsHeader(PresentationTheme, String) case session(index: Int32, theme: PresentationTheme, strings: PresentationStrings, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) + case website(index: Int32, theme: PresentationTheme, strings: PresentationStrings, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool) var section: ItemListSectionId { switch self { - case .currentSessionHeader, .currentSession, .terminateOtherSessions, .currentSessionInfo: + case .currentSessionHeader, .currentSession, .terminateOtherSessions, .terminateAllWebSessions, .currentSessionInfo: return RecentSessionsSection.currentSession.rawValue - case .otherSessionsHeader, .session: + case .otherSessionsHeader, .session, .website: return RecentSessionsSection.otherSessions.rawValue } } @@ -81,12 +94,16 @@ private enum RecentSessionsEntry: ItemListNodeEntry { return .index(1) case .terminateOtherSessions: return .index(2) - case .currentSessionInfo: + case .terminateAllWebSessions: return .index(3) - case .otherSessionsHeader: + case .currentSessionInfo: return .index(4) + case .otherSessionsHeader: + return .index(5) case let .session(_, _, _, session, _, _, _): return .session(session.hash) + case let .website(_, _, _, website, _, _, _, _): + return .session(website.hash) } } @@ -104,6 +121,12 @@ private enum RecentSessionsEntry: ItemListNodeEntry { } else { return false } + case let .terminateAllWebSessions(lhsTheme, lhsText): + if case let .terminateAllWebSessions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .currentSessionInfo(lhsTheme, lhsText): if case let .currentSessionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -128,6 +151,12 @@ private enum RecentSessionsEntry: ItemListNodeEntry { } else { return false } + case let .website(lhsIndex, lhsTheme, lhsStrings, lhsWebsite, lhsPeer, lhsEnabled, lhsEditing, lhsRevealed): + if case let .website(rhsIndex, rhsTheme, rhsStrings, rhsWebsite, rhsPeer, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsWebsite == rhsWebsite, arePeersEqual(lhsPeer, rhsPeer), lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { + return true + } else { + return false + } } } @@ -147,6 +176,12 @@ private enum RecentSessionsEntry: ItemListNodeEntry { } else { return false } + case let .website(lhsIndex, _, _, _, _, _, _, _): + if case let .website(rhsIndex, _, _, _, _, _, _, _) = rhs { + return lhsIndex <= rhsIndex + } else { + return false + } default: preconditionFailure() } @@ -165,6 +200,10 @@ private enum RecentSessionsEntry: ItemListNodeEntry { return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.terminateOtherSessions() }) + case let .terminateAllWebSessions(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.terminateAllWebSessions() + }) case let .currentSessionInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .otherSessionsHeader(theme, text): @@ -175,6 +214,12 @@ private enum RecentSessionsEntry: ItemListNodeEntry { }, removeSession: { id in arguments.removeSession(id) }) + case let .website(_, theme, strings, website, peer, enabled, editing, revealed): + return ItemListWebsiteItem(theme: theme, strings: strings, website: website, peer: peer, enabled: enabled, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in + arguments.setSessionIdWithRevealedOptions(previousId, id) + }, removeSession: { id in + arguments.removeWebSession(id) + }) } } } @@ -266,6 +311,34 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, return entries } +private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, websites: [WebAuthorization]?, peers: [PeerId : Peer]?) -> [RecentSessionsEntry] { + var entries: [RecentSessionsEntry] = [] + + if let websites = websites, let peers = peers { + var existingSessionIds = Set() + if websites.count > 0 { + entries.append(.terminateAllWebSessions(presentationData.theme, presentationData.strings.AuthSessions_LogOutApplications)) + entries.append(.currentSessionInfo(presentationData.theme, presentationData.strings.AuthSessions_LogOutApplicationsHelp)) + + entries.append(.otherSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_LoggedInWithTelegram)) + + let filteredWebsites: [WebAuthorization] = websites.sorted(by: { lhs, rhs in + return lhs.dateActive > rhs.dateActive + }) + + for i in 0 ..< filteredWebsites.count { + let website = websites[i] + if !existingSessionIds.contains(website.hash) { + existingSessionIds.insert(website.hash) + entries.append(.website(index: Int32(i), theme: presentationData.theme, strings: presentationData.strings, website: website, peer: peers[website.botId], enabled: state.removingSessionId != website.hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == website.hash)) + } + } + } + } + + return entries +} + public func recentSessionsController(account: Account) -> ViewController { let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: RecentSessionsControllerState()) @@ -283,7 +356,9 @@ public func recentSessionsController(account: Account) -> ViewController { let terminateOtherSessionsDisposable = MetaDisposable() actionsDisposable.add(terminateOtherSessionsDisposable) + let mode = ValuePromise(.sessions) let sessionsPromise = Promise<[RecentAccountSession]?>(nil) + let websitesPromise = Promise<([WebAuthorization], [PeerId : Peer])?>(nil) let arguments = RecentSessionsControllerArguments(account: account, setSessionIdWithRevealedOptions: { sessionId, fromSessionId in updateState { state in @@ -354,7 +429,7 @@ public func recentSessionsController(account: Account) -> ViewController { return .complete() } - terminateOtherSessionsDisposable.set((terminateOtherAccountSessions(account: account) |> then(applySessions)).start(error: { _ in + terminateOtherSessionsDisposable.set((terminateOtherAccountSessions(account: account) |> then(applySessions) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedTerminatingOtherSessions(false) } @@ -368,18 +443,95 @@ public func recentSessionsController(account: Account) -> ViewController { ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, removeWebSession: { sessionId in + updateState { + return $0.withUpdatedRemovingSessionId(sessionId) + } + + let applySessions: Signal = websitesPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { websitesAndPeers -> Signal in + if let websites = websitesAndPeers?.0, let peers = websitesAndPeers?.1 { + var updatedWebsites = websites + for i in 0 ..< updatedWebsites.count { + if updatedWebsites[i].hash == sessionId { + updatedWebsites.remove(at: i) + break + } + } + + if updatedWebsites.isEmpty { + mode.set(.sessions) + } + websitesPromise.set(.single((updatedWebsites, peers))) + } + + return .complete() + } + + removeSessionDisposable.set(((terminateWebSession(network: account.network, hash: sessionId) + |> mapToSignal { _ -> Signal in + return .complete() + }) |> then(applySessions) |> deliverOnMainQueue).start(error: { _ in + updateState { + return $0.withUpdatedRemovingSessionId(nil) + } + }, completed: { + updateState { + return $0.withUpdatedRemovingSessionId(nil) + } + })) + }, terminateAllWebSessions: { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let controller = ActionSheetController(presentationTheme: presentationData.theme) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.AuthSessions_LogOutApplications, color: .destructive, action: { + dismissAction() + + updateState { + return $0.withUpdatedTerminatingOtherSessions(true) + } + + terminateOtherSessionsDisposable.set((terminateAllWebSessions(network: account.network) |> deliverOnMainQueue).start(error: { _ in + updateState { + return $0.withUpdatedTerminatingOtherSessions(false) + } + }, completed: { + updateState { + return $0.withUpdatedTerminatingOtherSessions(false) + } + mode.set(.sessions) + websitesPromise.set(.single(([], [:]))) + })) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) let sessionsSignal: Signal<[RecentAccountSession]?, NoError> = .single(nil) |> then(requestRecentAccountSessions(account: account) |> map(Optional.init)) - sessionsPromise.set(sessionsSignal) - var previousSessions: [RecentAccountSession]? + let websitesSignal: Signal<([WebAuthorization], [PeerId : Peer])?, NoError> = .single(nil) |> then(webSessions(network: account.network) |> map(Optional.init)) + websitesPromise.set(websitesSignal) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), sessionsPromise.get()) + + let previousMode = Atomic(value: .sessions) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, mode.get(), statePromise.get(), sessionsPromise.get(), websitesPromise.get()) |> deliverOnMainQueue - |> map { presentationData, state, sessions -> (ItemListControllerState, (ItemListNodeState, RecentSessionsEntry.ItemGenerationArguments)) in + |> map { presentationData, mode, state, sessions, websitesAndPeers -> (ItemListControllerState, (ItemListNodeState, RecentSessionsEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? + let websites = websitesAndPeers?.0 + let peers = websitesAndPeers?.1 + if let sessions = sessions, sessions.count > 1 { if state.terminatingOtherSessions { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) @@ -403,11 +555,32 @@ public func recentSessionsController(account: Account) -> ViewController { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } - let previous = previousSessions - previousSessions = sessions + let title: ItemListControllerTitle + let entries: [RecentSessionsEntry] + if let websites = websites, !websites.isEmpty { + title = .sectionControl([presentationData.strings.AuthSessions_Sessions, presentationData.strings.AuthSessions_LoggedIn], mode.rawValue) + } else { + title = .text(presentationData.strings.AuthSessions_Title) + } - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.AuthSessions_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: recentSessionsControllerEntries(presentationData: presentationData, state: state, sessions: sessions), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && sessions != nil && previous!.count >= sessions!.count) + var animateChanges = true + switch (mode, websites, peers) { + case (.websites, let websites, let peers): + entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, websites: websites, peers: peers) + default: + entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, sessions: sessions) + } + + let previousMode = previousMode.swap(mode) + var crossfadeState = false + + if previousMode != mode { + crossfadeState = true + animateChanges = false + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(entries: entries, style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: crossfadeState, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -415,6 +588,9 @@ public func recentSessionsController(account: Account) -> ViewController { } let controller = ItemListController(account: account, state: signal) + controller.titleControlValueChanged = { [weak mode] index in + mode?.set(index == 0 ? .sessions : .websites) + } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) diff --git a/TelegramUI/SaveToCameraRoll.swift b/TelegramUI/SaveToCameraRoll.swift index 76cbc6df7b..4ad937e8c2 100644 --- a/TelegramUI/SaveToCameraRoll.swift +++ b/TelegramUI/SaveToCameraRoll.swift @@ -19,15 +19,15 @@ func saveToCameraRoll(applicationContext: TelegramApplicationContext, postbox: P isImage = false } } else if let webpage = mediaReference.media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { - if let image = content.image { - if let representation = largestImageRepresentation(image.representations) { - resource = representation.resource - } - } else if let file = content.file { + if let file = content.file { resource = file.resource if file.isVideo { isImage = false } + } else if let image = content.image { + if let representation = largestImageRepresentation(image.representations) { + resource = representation.resource + } } } diff --git a/TelegramUI/SecureIdAuthFormContentNode.swift b/TelegramUI/SecureIdAuthFormContentNode.swift index 1c1e518294..b426f1f08e 100644 --- a/TelegramUI/SecureIdAuthFormContentNode.swift +++ b/TelegramUI/SecureIdAuthFormContentNode.swift @@ -4,6 +4,7 @@ import Display import Postbox import TelegramCore +private let infoFont = Font.regular(14.0) private let passwordFont = Font.regular(16.0) private let buttonFont = Font.regular(17.0) @@ -32,23 +33,25 @@ final class SecureIdAuthFormContentNode: ASDisplayNode, SecureIdAuthContentNode, self.headerNode = ImmediateTextNode() self.headerNode.displaysAsynchronously = false - self.headerNode.attributedText = NSAttributedString(string: strings.Passport_RequestedInformation, font: Font.regular(14.0), textColor: theme.list.sectionHeaderTextColor) + self.headerNode.attributedText = NSAttributedString(string: strings.Passport_RequestedInformation, font: infoFont, textColor: theme.list.sectionHeaderTextColor) self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false self.textNode.maximumNumberOfLines = 0 self.textNode.lineSpacing = 0.2 - let text = NSMutableAttributedString() - let textData = strings.Passport_AcceptHelp(peer.displayTitle, "@" + (peer.addressName ?? "")) - text.append(NSAttributedString(string: textData.0, font: Font.regular(14.0), textColor: theme.list.freeTextColor)) - for (index, range) in textData.1 { - if index == 2 { - text.addAttribute(.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue, range: range) - text.addAttribute(.foregroundColor, value: theme.list.itemAccentColor, range: range) - if let privacyPolicyUrl = privacyPolicyUrl { - text.addAttribute(NSAttributedStringKey(rawValue: TelegramTextAttributes.URL), value: privacyPolicyUrl, range: range) - } - } + + let text: NSAttributedString + if let privacyPolicyUrl = privacyPolicyUrl { + let privacyPolicyAttributes = MarkdownAttributeSet(font: infoFont, textColor: theme.list.freeTextColor) + let privacyPolicyLinkAttributes = MarkdownAttributeSet(font: infoFont, textColor: theme.list.itemAccentColor, additionalAttributes: [NSAttributedStringKey.underlineStyle.rawValue: NSUnderlineStyle.styleSingle.rawValue as NSNumber, TelegramTextAttributes.URL: privacyPolicyUrl]) + + text = parseMarkdownIntoAttributedString(strings.Passport_PrivacyPolicy(peer.displayTitle, (peer.addressName ?? "")).0.replacingOccurrences(of: "]", with: "]()"), attributes: MarkdownAttributes(body: privacyPolicyAttributes, bold: privacyPolicyAttributes, link: privacyPolicyLinkAttributes, linkAttribute: { _ in + return nil + }), textAlignment: .center) + + + } else { + text = NSAttributedString(string: strings.Passport_AcceptHelp(peer.displayTitle, (peer.addressName ?? "")).0, font: infoFont, textColor: theme.list.freeTextColor, paragraphAlignment: .left) } self.textNode.attributedText = text diff --git a/TelegramUI/SecureIdDocumentFormControllerNode.swift b/TelegramUI/SecureIdDocumentFormControllerNode.swift index aee80dc750..c9394d28f0 100644 --- a/TelegramUI/SecureIdDocumentFormControllerNode.swift +++ b/TelegramUI/SecureIdDocumentFormControllerNode.swift @@ -80,17 +80,22 @@ private struct SecureIdDocumentFormIdentityDetailsState: Equatable { var gender: SecureIdGender? func isComplete() -> Bool { - if self.firstName.isEmpty { + let nameMaxLength = 255 + + if self.firstName.isEmpty || self.firstName.count > nameMaxLength { return false } - if self.lastName.isEmpty { + if self.middleName.count > nameMaxLength { return false } - if self.nativeNameRequired && self.primaryLanguageByCountry[self.countryCode] != "en" { - if self.nativeFirstName.isEmpty { + if self.lastName.isEmpty || self.lastName.count > nameMaxLength { + return false + } + if self.nativeNameRequired && self.primaryLanguageByCountry[self.residenceCountryCode] != "en" { + if self.nativeFirstName.isEmpty || self.nativeFirstName.count > nameMaxLength { return false } - if self.nativeLastName.isEmpty { + if self.nativeLastName.isEmpty || self.nativeLastName.count > nameMaxLength { return false } } @@ -116,7 +121,9 @@ private struct SecureIdDocumentFormIdentityDocumentState: Equatable { var expiryDate: SecureIdDate? func isComplete() -> Bool { - if self.identifier.isEmpty { + let identifierMaxLength = 24 + + if self.identifier.isEmpty || self.identifier.count > identifierMaxLength { return false } return true @@ -143,13 +150,11 @@ private struct SecureIdDocumentFormIdentityState { return false } } - if let document = self.document { if !document.isComplete() { return false } } - return true } } @@ -163,16 +168,23 @@ private struct SecureIdDocumentFormAddressDetailsState: Equatable { var postcode: String func isComplete() -> Bool { + let cityMinLength = 2 + let stateMinLength = 2 + let postcodeMaxLength = 12 + if self.street1.isEmpty { return false } - if self.city.isEmpty { + if self.city.count < cityMinLength { return false } if self.countryCode.isEmpty { return false } - if self.postcode.isEmpty { + if self.countryCode == "US" && self.state.count < stateMinLength { + return false + } + if self.postcode.isEmpty || self.postcode.count > postcodeMaxLength { return false } return true @@ -199,7 +211,6 @@ private struct SecureIdDocumentFormAddressState { return false } } - return true } } @@ -397,7 +408,6 @@ struct SecureIdDocumentFormState: FormControllerInnerState { var errorIndex = 0 if let details = identity.details { - result.append(.entry(SecureIdDocumentFormEntry.infoHeader(.identity))) result.append(.entry(SecureIdDocumentFormEntry.firstName(details.firstName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.firstName))]))) result.append(.entry(SecureIdDocumentFormEntry.middleName(details.middleName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.middleName))]))) @@ -405,19 +415,19 @@ struct SecureIdDocumentFormState: FormControllerInnerState { result.append(.entry(SecureIdDocumentFormEntry.birthdate(details.birthdate, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.birthdate))]))) result.append(.entry(SecureIdDocumentFormEntry.gender(details.gender, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.gender))]))) - result.append(.entry(SecureIdDocumentFormEntry.countryCode(details.countryCode, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.countryCode))]))) + result.append(.entry(SecureIdDocumentFormEntry.countryCode(.identity, details.countryCode, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.countryCode))]))) result.append(.entry(SecureIdDocumentFormEntry.residenceCountryCode(details.residenceCountryCode, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.residenceCountryCode))]))) - if details.nativeNameRequired && !details.countryCode.isEmpty && details.primaryLanguageByCountry[details.countryCode] != "en" { + if details.nativeNameRequired && !details.residenceCountryCode.isEmpty && details.primaryLanguageByCountry[details.residenceCountryCode] != "en" { if let last = result.last, case .spacer = last { } else { result.append(.spacer) } - result.append(.entry(SecureIdDocumentFormEntry.nativeInfoHeader(details.primaryLanguageByCountry[details.countryCode] ?? ""))) + result.append(.entry(SecureIdDocumentFormEntry.nativeInfoHeader(details.primaryLanguageByCountry[details.residenceCountryCode] ?? ""))) result.append(.entry(SecureIdDocumentFormEntry.nativeFirstName(details.nativeFirstName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.firstNameNative))]))) result.append(.entry(SecureIdDocumentFormEntry.nativeMiddleName(details.nativeMiddleName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.middleNameNative))]))) result.append(.entry(SecureIdDocumentFormEntry.nativeLastName(details.nativeLastName, self.previousValues[.personalDetails]?.errors[.field(.personalDetails(.lastNameNative))]))) - result.append(.entry(SecureIdDocumentFormEntry.nativeInfo(details.primaryLanguageByCountry[details.countryCode] ?? ""))) + result.append(.entry(SecureIdDocumentFormEntry.nativeInfo(details.primaryLanguageByCountry[details.residenceCountryCode] ?? "", details.residenceCountryCode))) result.append(.spacer) } } @@ -715,7 +725,7 @@ struct SecureIdDocumentFormState: FormControllerInnerState { result.append(.entry(SecureIdDocumentFormEntry.street2(details.street2, self.previousValues[.address]?.errors[.field(.address(.streetLine2))]))) result.append(.entry(SecureIdDocumentFormEntry.city(details.city, self.previousValues[.address]?.errors[.field(.address(.city))]))) result.append(.entry(SecureIdDocumentFormEntry.state(details.state, self.previousValues[.address]?.errors[.field(.address(.state))]))) - result.append(.entry(SecureIdDocumentFormEntry.countryCode(details.countryCode, self.previousValues[.address]?.errors[.field(.address(.countryCode))]))) + result.append(.entry(SecureIdDocumentFormEntry.countryCode(.address, details.countryCode, self.previousValues[.address]?.errors[.field(.address(.countryCode))]))) result.append(.entry(SecureIdDocumentFormEntry.postcode(details.postcode, self.previousValues[.address]?.errors[.field(.address(.postCode))]))) } @@ -818,49 +828,51 @@ struct SecureIdDocumentFormState: FormControllerInnerState { } } - if self.selfieRequired { - if let selfieDocument = self.selfieDocument { - switch selfieDocument { + func isDocumentReady(_ document: SecureIdVerificationDocument?) -> Bool { + if let document = document { + switch document { case let .local(local): switch local.state { case .uploading: - return .saveNotAvailable + return false case .uploaded: - break + return true } case .remote: - break - } + return true + } } else { + return false + } + } + + if self.frontSideRequired { + guard isDocumentReady(self.frontSideDocument) else { + return .saveNotAvailable + } + } + + if self.backSideRequired { + guard isDocumentReady(self.backSideDocument) else { + return .saveNotAvailable + } + } + + if self.selfieRequired { + guard isDocumentReady(self.selfieDocument) else { return .saveNotAvailable } } for document in self.documents { - switch document { - case let .local(local): - switch local.state { - case .uploading: - return .saveNotAvailable - case .uploaded: - break - } - case .remote: - break + guard isDocumentReady(document) else { + return .saveNotAvailable } } for document in self.translations { - switch document { - case let .local(local): - switch local.state { - case .uploading: - return .saveNotAvailable - case .uploaded: - break - } - case .remote: - break + guard isDocumentReady(document) else { + return .saveNotAvailable } } @@ -1210,9 +1222,9 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { case nativeFirstName(String, String?) case nativeMiddleName(String, String?) case nativeLastName(String, String?) - case nativeInfo(String) + case nativeInfo(String, String) case gender(SecureIdGender?, String?) - case countryCode(String, String?) + case countryCode(SecureIdDocumentFormEntryCategory, String, String?) case residenceCountryCode(String, String?) case birthdate(SecureIdDate?, String?) case expiryDate(SecureIdDate?, String?) @@ -1389,8 +1401,8 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { } else { return false } - case let .nativeInfo(language): - if case .nativeInfo(language) = to { + case let .nativeInfo(language, countryCode): + if case .nativeInfo(language, countryCode) = to { return true } else { return false @@ -1401,8 +1413,8 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { } else { return false } - case let .countryCode(value, error): - if case .countryCode(value, error) = to { + case let .countryCode(category, value, error): + if case .countryCode(category, value, error) = to { return true } else { return false @@ -1590,12 +1602,13 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { return FormControllerTextInputItem(title: strings.Passport_Identity_Surname, text: value, placeholder: strings.Passport_Identity_SurnamePlaceholder, type: .regular(capitalization: .words, autocorrection: false), error: error, textUpdated: { text in params.updateText(.nativeLastName, text) }) - case let .nativeInfo(language): + case let .nativeInfo(language, countryCode): let text: String if !language.isEmpty, let _ = strings.dict["Passport.Language.\(language)"] { text = strings.Passport_Identity_NativeNameHelp } else { - text = strings.Passport_Identity_NativeNameGenericHelp("").0 + let countryName = AuthorizationSequenceCountrySelectionController.lookupCountryNameById(countryCode.uppercased(), strings: strings) ?? "" + text = strings.Passport_Identity_NativeNameGenericHelp(countryName).0 } return FormControllerTextItem(text: text) case let .gender(value, error): @@ -1611,8 +1624,18 @@ enum SecureIdDocumentFormEntry: FormControllerEntry { return FormControllerDetailActionItem(title: strings.Passport_Identity_Gender, text: text, placeholder: strings.Passport_Identity_GenderPlaceholder, error: error, activated: { params.activateSelection(.gender) }) - case let .countryCode(value, error): - return FormControllerDetailActionItem(title: strings.Passport_Identity_Country, text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: strings.Passport_Identity_CountryPlaceholder, error: error, activated: { + case let .countryCode(category, value, error): + let title: String + let placeholder: String + switch category { + case .identity: + title = strings.Passport_Identity_Country + placeholder = strings.Passport_Identity_CountryPlaceholder + case .address: + title = strings.Passport_Address_Country + placeholder = strings.Passport_Address_CountryPlaceholder + } + return FormControllerDetailActionItem(title: title, text: AuthorizationSequenceCountrySelectionController.lookupCountryNameById(value.uppercased(), strings: strings) ?? "", placeholder: placeholder, error: error, activated: { params.activateSelection(.country) }) case let .residenceCountryCode(value, error): diff --git a/TelegramUI/SecureIdPlaintextFormControllerNode.swift b/TelegramUI/SecureIdPlaintextFormControllerNode.swift index c2ef640be8..f8eb355011 100644 --- a/TelegramUI/SecureIdPlaintextFormControllerNode.swift +++ b/TelegramUI/SecureIdPlaintextFormControllerNode.swift @@ -489,7 +489,7 @@ enum SecureIdPlaintextFormEntry: FormControllerEntry { func item(params: SecureIdPlaintextFormParams, strings: PresentationStrings) -> FormControllerItem { switch self { case let .immediatelyAvailablePhone(value): - return FormControllerActionItem(type: .accent, title: formatPhoneNumber(value), activated: { + return FormControllerActionItem(type: .accent, title: strings.Passport_Phone_UseTelegramNumber(formatPhoneNumber(value)).0, activated: { params.usePhone(value) }) case .immediatelyAvailablePhoneInfo: diff --git a/TelegramUI/SelectivePrivacySettingsController.swift b/TelegramUI/SelectivePrivacySettingsController.swift index f6b11d6d5d..f50547ac38 100644 --- a/TelegramUI/SelectivePrivacySettingsController.swift +++ b/TelegramUI/SelectivePrivacySettingsController.swift @@ -34,17 +34,25 @@ private final class SelectivePrivacySettingsControllerArguments { let openEnableFor: () -> Void let openDisableFor: () -> Void - init(account: Account, updateType: @escaping (SelectivePrivacySettingType) -> Void, openEnableFor: @escaping () -> Void, openDisableFor: @escaping () -> Void) { + let updateCallsP2PMode: ((VoiceCallP2PMode) -> Void)? + let updateCallsIntegrationEnabled: ((Bool) -> Void)? + + init(account: Account, updateType: @escaping (SelectivePrivacySettingType) -> Void, openEnableFor: @escaping () -> Void, openDisableFor: @escaping () -> Void, updateCallsP2PMode: ((VoiceCallP2PMode) -> Void)?, updateCallsIntegrationEnabled: ((Bool) -> Void)?) { self.account = account self.updateType = updateType self.openEnableFor = openEnableFor self.openDisableFor = openDisableFor + + self.updateCallsP2PMode = updateCallsP2PMode + self.updateCallsIntegrationEnabled = updateCallsIntegrationEnabled } } private enum SelectivePrivacySettingsSection: Int32 { case setting case peers + case callsP2P + case callsIntegrationEnabled } private func stringForUserCount(_ count: Int, strings: PresentationStrings) -> String { @@ -64,6 +72,13 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { case disableFor(PresentationTheme, String, String) case enableFor(PresentationTheme, String, String) case peersInfo(PresentationTheme, String) + case callsP2PHeader(PresentationTheme, String) + case callsP2PAlways(PresentationTheme, String, Bool) + case callsP2PContacts(PresentationTheme, String, Bool) + case callsP2PNever(PresentationTheme, String, Bool) + case callsP2PInfo(PresentationTheme, String) + case callsIntegrationEnabled(PresentationTheme, String, Bool) + case callsIntegrationInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -71,6 +86,10 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { return SelectivePrivacySettingsSection.setting.rawValue case .disableFor, .enableFor, .peersInfo: return SelectivePrivacySettingsSection.peers.rawValue + case .callsP2PHeader, .callsP2PAlways, .callsP2PContacts, .callsP2PNever, .callsP2PInfo: + return SelectivePrivacySettingsSection.callsP2P.rawValue + case .callsIntegrationEnabled, .callsIntegrationInfo: + return SelectivePrivacySettingsSection.callsIntegrationEnabled.rawValue } } @@ -92,6 +111,20 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { return 6 case .peersInfo: return 7 + case .callsP2PHeader: + return 8 + case .callsP2PAlways: + return 9 + case .callsP2PContacts: + return 10 + case .callsP2PNever: + return 11 + case .callsP2PInfo: + return 12 + case .callsIntegrationEnabled: + return 13 + case .callsIntegrationInfo: + return 14 } } @@ -145,6 +178,48 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { } else { return false } + case let .callsP2PHeader(lhsTheme, lhsText): + if case let .callsP2PHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .callsP2PInfo(lhsTheme, lhsText): + if case let .callsP2PInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .callsP2PAlways(lhsTheme, lhsText, lhsValue): + if case let .callsP2PAlways(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .callsP2PContacts(lhsTheme, lhsText, lhsValue): + if case let .callsP2PContacts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .callsP2PNever(lhsTheme, lhsText, lhsValue): + if case let .callsP2PNever(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .callsIntegrationEnabled(lhsTheme, lhsText, lhsValue): + if case let .callsIntegrationEnabled(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .callsIntegrationInfo(lhsTheme, lhsText): + if case let .callsIntegrationInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } @@ -180,6 +255,28 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { }) case let .peersInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .callsP2PHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .callsP2PAlways(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.updateCallsP2PMode?(.always) + }) + case let .callsP2PContacts(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.updateCallsP2PMode?(.contacts) + }) + case let .callsP2PNever(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.updateCallsP2PMode?(.never) + }) + case let .callsP2PInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .callsIntegrationEnabled(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.updateCallsIntegrationEnabled?(value) + }) + case let .callsIntegrationInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -191,11 +288,20 @@ private struct SelectivePrivacySettingsControllerState: Equatable { let saving: Bool - init(setting: SelectivePrivacySettingType, enableFor: Set, disableFor: Set, saving: Bool) { + let callDataSaving: VoiceCallDataSaving? + let callP2PMode: VoiceCallP2PMode? + let callIntegrationAvailable: Bool? + let callIntegrationEnabled: Bool? + + init(setting: SelectivePrivacySettingType, enableFor: Set, disableFor: Set, saving: Bool, callDataSaving: VoiceCallDataSaving?, callP2PMode: VoiceCallP2PMode?, callIntegrationAvailable: Bool?, callIntegrationEnabled: Bool?) { self.setting = setting self.enableFor = enableFor self.disableFor = disableFor self.saving = saving + self.callDataSaving = callDataSaving + self.callP2PMode = callP2PMode + self.callIntegrationAvailable = callIntegrationAvailable + self.callIntegrationEnabled = callIntegrationEnabled } static func ==(lhs: SelectivePrivacySettingsControllerState, rhs: SelectivePrivacySettingsControllerState) -> Bool { @@ -211,24 +317,44 @@ private struct SelectivePrivacySettingsControllerState: Equatable { if lhs.saving != rhs.saving { return false } + if lhs.callDataSaving != rhs.callDataSaving { + return false + } + if lhs.callP2PMode != rhs.callP2PMode { + return false + } + if lhs.callIntegrationAvailable != rhs.callIntegrationAvailable { + return false + } + if lhs.callIntegrationEnabled != rhs.callIntegrationEnabled { + return false + } return true } func withUpdatedSetting(_ setting: SelectivePrivacySettingType) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving) + return SelectivePrivacySettingsControllerState(setting: setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled) } func withUpdatedEnableFor(_ enableFor: Set) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: enableFor, disableFor: self.disableFor, saving: self.saving) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled) } func withUpdatedDisableFor(_ disableFor: Set) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: disableFor, saving: self.saving) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled) } func withUpdatedSaving(_ saving: Bool) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: saving) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled) + } + + func withUpdatedCallsP2PMode(_ mode: VoiceCallP2PMode) -> SelectivePrivacySettingsControllerState { + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: mode, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: self.callIntegrationEnabled) + } + + func withUpdatedCallsIntegrationEnabled(_ enabled: Bool) -> SelectivePrivacySettingsControllerState { + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callDataSaving: self.callDataSaving, callP2PMode: self.callP2PMode, callIntegrationAvailable: self.callIntegrationAvailable, callIntegrationEnabled: enabled) } } @@ -280,10 +406,25 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present } entries.append(.peersInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_CustomShareSettingsHelp)) + if case .voiceCalls = kind, let p2pMode = state.callP2PMode, let integrationAvailable = state.callIntegrationAvailable, let integrationEnabled = state.callIntegrationEnabled { + entries.append(.callsP2PHeader(presentationData.theme, presentationData.strings.Privacy_Calls_P2P.uppercased())) + + entries.append(.callsP2PAlways(presentationData.theme, presentationData.strings.Privacy_Calls_P2PAlways, p2pMode == .always)) + entries.append(.callsP2PContacts(presentationData.theme, presentationData.strings.Privacy_Calls_P2PContacts, p2pMode == .contacts)) + entries.append(.callsP2PNever(presentationData.theme, presentationData.strings.Privacy_Calls_P2PNever, p2pMode == .never)) + + entries.append(.callsP2PInfo(presentationData.theme, presentationData.strings.Privacy_Calls_P2PHelp)) + + if integrationAvailable { + entries.append(.callsIntegrationEnabled(presentationData.theme, presentationData.strings.Privacy_Calls_Integration, integrationEnabled)) + entries.append(.callsIntegrationInfo(presentationData.theme, presentationData.strings.Privacy_Calls_IntegrationHelp)) + } + } + return entries } -func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacySettingsKind, current: SelectivePrivacySettings, updated: @escaping (SelectivePrivacySettings) -> Void) -> ViewController { +func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacySettingsKind, current: SelectivePrivacySettings, callSettings: VoiceCallSettings? = nil, callIntegrationAvailable: Bool? = nil, updated: @escaping (SelectivePrivacySettings, VoiceCallSettings?) -> Void) -> ViewController { let strings = account.telegramApplicationContext.currentPresentationData.with { $0 }.strings var initialEnableFor = Set() @@ -297,7 +438,7 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy case let .enableEveryone(disableFor): initialDisableFor = disableFor } - let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, saving: false) + let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, saving: false, callDataSaving: callSettings?.dataSaving, callP2PMode: callSettings?.p2pMode, callIntegrationAvailable: callIntegrationAvailable, callIntegrationEnabled: callSettings?.enableSystemIntegration) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -307,7 +448,6 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy var dismissImpl: (() -> Void)? var pushControllerImpl: ((ViewController) -> Void)? - var presentControllerImpl: ((ViewController) -> Void)? let actionsDisposable = DisposableSet() @@ -358,6 +498,14 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy return state.withUpdatedDisableFor(Set(updatedPeerIds)).withUpdatedEnableFor(state.enableFor.subtracting(Set(updatedPeerIds))) } })) + }, updateCallsP2PMode: { mode in + updateState { state in + return state.withUpdatedCallsP2PMode(mode) + } + }, updateCallsIntegrationEnabled: { enabled in + updateState { state in + return state.withUpdatedCallsIntegrationEnabled(enabled) + } }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue @@ -402,7 +550,11 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy updateState { state in return state.withUpdatedSaving(false) } - updated(settings) + if case .voiceCalls = kind, let dataSaving = state.callDataSaving, let p2pMode = state.callP2PMode, let systemIntegrationEnabled = state.callIntegrationEnabled { + updated(settings, VoiceCallSettings(dataSaving: dataSaving, p2pMode: p2pMode, enableSystemIntegration: systemIntegrationEnabled)) + } else { + updated(settings, nil) + } dismissImpl?() })) } @@ -430,9 +582,6 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } - presentControllerImpl = { [weak controller] c in - controller?.present(c, in: .window(.root)) - } dismissImpl = { [weak controller] in let _ = (controller?.navigationController as? NavigationController)?.popViewController(animated: true) } diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 1c62710f97..dd99b17d5f 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -403,6 +403,7 @@ public func settingsController(account: Account, accountManager: AccountManager) var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? + var getNavigationControllerImpl: (() -> NavigationController?)? let actionsDisposable = DisposableSet() @@ -431,19 +432,24 @@ public func settingsController(account: Account, accountManager: AccountManager) let archivedPacks = Promise<[ArchivedStickerPackItem]?>() - - let openFaq: () -> Void = { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - var faqUrl = presentationData.strings.Settings_FAQ_URL - if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { - faqUrl = "https://telegram.org/faq#general" - } - - if let applicationContext = account.applicationContext as? TelegramApplicationContext { - applicationContext.applicationBindings.openUrl(faqUrl) - } + let openFaq: (Promise) -> Void = { resolvedUrl in + let _ = (resolvedUrl.get() + |> take(1) + |> deliverOnMainQueue).start(next: { resolvedUrl in + openResolvedUrl(resolvedUrl, account: account, navigationController: getNavigationControllerImpl?(), openPeer: { peer, navigation in + + }, present: { controller, arguments in + pushControllerImpl?(controller) + }, dismissInput: {}) + }) } + var faqUrl = account.telegramApplicationContext.currentPresentationData.with { $0 }.strings.Settings_FAQ_URL + if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { + faqUrl = "https://telegram.org/faq#general" + } + let resolvedUrl = resolveInstantViewUrl(account: account, url: faqUrl) + let arguments = SettingsItemArguments(account: account, accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { var updating = false updateState { @@ -501,9 +507,13 @@ public func settingsController(account: Account, accountManager: AccountManager) let supportPeer = Promise() supportPeer.set(supportPeerId(account: account)) let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + let resolvedUrlPromise = Promise() + resolvedUrlPromise.set(resolvedUrl) + presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: presentationData.strings.Settings_FAQ_Intro, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { - openFaq() + openFaq(resolvedUrlPromise) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).start(next: { peerId in @@ -514,7 +524,10 @@ public func settingsController(account: Account, accountManager: AccountManager) }) ]), nil) }, openFaq: { - openFaq() + let resolvedUrlPromise = Promise() + resolvedUrlPromise.set(resolvedUrl) + + openFaq(resolvedUrlPromise) }, openEditing: { let _ = (account.postbox.transaction { transaction -> (Peer?, CachedPeerData?) in return (transaction.getPeer(account.peerId), transaction.getPeerCachedData(peerId: account.peerId)) @@ -652,6 +665,9 @@ public func settingsController(account: Account, accountManager: AccountManager) presentControllerImpl = { [weak controller] value, arguments in controller?.present(value, in: .window(.root), with: arguments ?? ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } + getNavigationControllerImpl = { [weak controller] in + return (controller?.navigationController as? NavigationController) + } avatarGalleryTransitionArguments = { [weak controller] entry in if let controller = controller { var result: ((ASDisplayNode, () -> UIView?), CGRect)? diff --git a/TelegramUI/ShareController.swift b/TelegramUI/ShareController.swift index 71970ac1b5..30c3338758 100644 --- a/TelegramUI/ShareController.swift +++ b/TelegramUI/ShareController.swift @@ -248,7 +248,10 @@ public final class ShareController: ViewController { } if case let .custom(action) = preferredAction { - self.defaultAction = action + self.defaultAction = ShareControllerAction(title: action.title, action: { [weak self] in + self?.controllerNode.cancel?() + action.action() + }) } self.peers.set(combineLatest(account.postbox.loadedPeerWithId(account.peerId) |> take(1), account.viewTracker.tailChatListView(groupId: nil, count: 150) |> take(1)) |> map { accountPeer, view -> ([Peer], Peer) in diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index 7f02de93b9..dfa7244619 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -505,7 +505,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } else { text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count) } - self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) + self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.destructiveActionTextColor, for: .normal) } else { let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { diff --git a/TelegramUI/UniversalVideoCalleryItem.swift b/TelegramUI/UniversalVideoCalleryItem.swift index 00f487051f..abbcab8237 100644 --- a/TelegramUI/UniversalVideoCalleryItem.swift +++ b/TelegramUI/UniversalVideoCalleryItem.swift @@ -281,9 +281,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusDisposable.set((videoNode.status |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { var initialBuffering = false + var buffering = false var isPaused = true var seekable = false + var hasStarted = false if let value = value { + hasStarted = value.timestamp > 0 + if let zoomableContent = strongSelf.zoomableContent, !value.dimensions.width.isZero && !value.dimensions.height.isZero { let videoSize = CGSize(width: value.dimensions.width * 2.0, height: value.dimensions.height * 2.0) if !zoomableContent.0.equalTo(videoSize) { @@ -296,6 +300,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { isPaused = false case let .buffering(_, whilePlaying): initialBuffering = true + buffering = true isPaused = !whilePlaying if let content = item.content as? NativeVideoContent, !content.streamVideo { initialBuffering = false @@ -310,13 +315,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } } - seekable = value.duration >= 44.0 + seekable = value.duration >= 45.0 } if initialBuffering { strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false), animated: false, completion: {}) } else { - strongSelf.statusNode.transitionToState(.play(.white), animated: false, completion: {}) } @@ -330,7 +334,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { strongSelf.footerContentNode.content = .info } else if isPaused { - if strongSelf.didPause { + if hasStarted || strongSelf.didPause || buffering { strongSelf.footerContentNode.content = .playback(paused: true, seekable: seekable) } else { strongSelf.footerContentNode.content = .info diff --git a/TelegramUI/UrlHandling.swift b/TelegramUI/UrlHandling.swift index 6bec32b68f..03383c42e2 100644 --- a/TelegramUI/UrlHandling.swift +++ b/TelegramUI/UrlHandling.swift @@ -166,27 +166,31 @@ func resolveUrl(account: Account, url: String) -> Signal { for scheme in schemes { let basePrefix = scheme + basePath + "/" if url.lowercased().hasPrefix(basePrefix) { - return webpagePreview(account: account, url: url) - |> map { webpage -> ResolvedUrl in - if let webpage = webpage, case let .Loaded(content) = webpage.content, content.instantPage != nil { - var anchorValue: String? - if let anchorRange = url.range(of: "#") { - let anchor = url[anchorRange.upperBound...] - if !anchor.isEmpty { - anchorValue = String(anchor) - } - } - return .instantView(webpage, anchorValue) - } else { - return .externalUrl(url) - } - } + return resolveInstantViewUrl(account: account, url: url) } } } return .single(.externalUrl(url)) } +func resolveInstantViewUrl(account: Account, url: String) -> Signal { + return webpagePreview(account: account, url: url) + |> map { webpage -> ResolvedUrl in + if let webpage = webpage, case let .Loaded(content) = webpage.content, content.instantPage != nil { + var anchorValue: String? + if let anchorRange = url.range(of: "#") { + let anchor = url[anchorRange.upperBound...] + if !anchor.isEmpty { + anchorValue = String(anchor) + } + } + return .instantView(webpage, anchorValue) + } else { + return .externalUrl(url) + } + } +} + /*private final class SafariLegacyPresentedController: LegacyPresentedController, SFSafariViewControllerDelegate { @available(iOSApplicationExtension 9.0, *) init(legacyController: SFSafariViewController) { diff --git a/TelegramUI/VoiceCallSettings.swift b/TelegramUI/VoiceCallSettings.swift index 08cf60b40c..95d105a6a2 100644 --- a/TelegramUI/VoiceCallSettings.swift +++ b/TelegramUI/VoiceCallSettings.swift @@ -20,7 +20,7 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable { public var enableSystemIntegration: Bool public static var defaultSettings: VoiceCallSettings { - return VoiceCallSettings(dataSaving: .never, p2pMode: .always, enableSystemIntegration: true) + return VoiceCallSettings(dataSaving: .never, p2pMode: .contacts, enableSystemIntegration: true) } init(dataSaving: VoiceCallDataSaving, p2pMode: VoiceCallP2PMode, enableSystemIntegration: Bool) { @@ -31,7 +31,7 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable { public init(decoder: PostboxDecoder) { self.dataSaving = VoiceCallDataSaving(rawValue: decoder.decodeInt32ForKey("ds", orElse: 0))! - self.p2pMode = VoiceCallP2PMode(rawValue: decoder.decodeInt32ForKey("p2pMode", orElse: 2))! + self.p2pMode = VoiceCallP2PMode(rawValue: decoder.decodeInt32ForKey("p2pMode", orElse: 1))! self.enableSystemIntegration = decoder.decodeInt32ForKey("enableSystemIntegration", orElse: 1) != 0 } diff --git a/TelegramUI/YoutubeEmbedImplementation.swift b/TelegramUI/YoutubeEmbedImplementation.swift index 24ef384627..689510fb03 100644 --- a/TelegramUI/YoutubeEmbedImplementation.swift +++ b/TelegramUI/YoutubeEmbedImplementation.swift @@ -169,6 +169,8 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation { if let eval = evalImpl { eval("play();") } + + self.ignorePosition = 2 } func pause() { @@ -194,6 +196,8 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation { if let updateStatus = self.updateStatus { updateStatus(self.status) } + + self.ignorePosition = 2 } func pageReady() { @@ -245,14 +249,18 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation { let playbackStatus: MediaPlayerPlaybackStatus switch playback { case 0: - playbackStatus = .paused - newTimestamp = 0.0 + if newTimestamp > Double(duration) - 1.0 { + playbackStatus = .paused + newTimestamp = 0.0 + } else { + playbackStatus = .buffering(initial: false, whilePlaying: true) + } case 1: playbackStatus = .playing case 2: playbackStatus = .paused case 3: - playbackStatus = .buffering(initial: false, whilePlaying: false) + playbackStatus = .buffering(initial: false, whilePlaying: true) default: playbackStatus = .buffering(initial: true, whilePlaying: false) }