diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 20d65681dc..a960050c2a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12063,7 +12063,14 @@ Sorry for the inconvenience."; "Login.EnterPhraseBeginningText" = "We've sent you an SMS with a secret phrase that starts with **\"%1$@\"** to your phone **%2$@**."; "Login.EnterPhrasePlaceholder" = "Enter Phrase from SMS"; +"Login.EnterPhraseHint" = "You can copy and paste the phrase here."; +"Login.BackToWord" = "Back to entering word"; +"Login.BackToPhrase" = "Back to entering phrase"; + "Login.WrongPhraseError" = "Incorrect, please try again."; +"Login.Paste" = "Paste"; + +"Login.ReturnToCode" = "Return to entering the code"; "Map.LiveLocationPrivateNewDescription" = "Choose for how long **%@** will see your accurate location, including when the app is closed."; "Map.LiveLocationGroupNewDescription" = "Choose for how long people in this chat will see your accurate location, including when the app is closed."; @@ -12077,3 +12084,8 @@ Sorry for the inconvenience."; "Map.LiveLocationForHours_any" = "For %@ hours"; "Map.LiveLocationIndefinite" = "Until I turn it off"; + +"Map.TapToAddTime" = "tap to add time"; +"Map.SharingLocation" = "Sharing Location..."; + +"Channel.AdminLog.Settings" = "Settings"; diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift index acd0e16dca..f125c86064 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift @@ -23,10 +23,11 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { var reset: (() -> Void)? public var requestNextOption: (() -> Void)? + public var requestPreviousOption: (() -> Void)? var resetEmail: (() -> Void)? var retryResetEmail: (() -> Void)? - var data: (String, String?, SentAuthorizationCodeType, AuthorizationCodeNextType?, Int32?)? + var data: (String, String?, SentAuthorizationCodeType, AuthorizationCodeNextType?, Int32?, Bool, Bool)? var termsOfService: (UnauthorizedAccountTermsOfService, Bool)? private let hapticFeedback = HapticFeedback() @@ -40,6 +41,19 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { } } + var isWordOrPhrase: Bool { + if let type = self.data?.2 { + switch type { + case .word, .phrase: + return true + default: + return false + } + } else { + return false + } + } + public init(presentationData: PresentationData, back: @escaping () -> Void) { self.strings = presentationData.strings self.theme = presentationData.theme @@ -60,7 +74,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { let proceed: String let stop: String - if let (_, _, type, _, _) = self?.data, case .email = type { + if let (_, _, type, _, _, _, _) = self?.data, case .email = type { text = presentationData.strings.Login_CancelEmailVerification proceed = presentationData.strings.Login_CancelEmailVerificationContinue stop = presentationData.strings.Login_CancelEmailVerificationStop @@ -107,6 +121,10 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self?.requestNextOption?() } + self.controllerNode.requestPreviousOption = { [weak self] in + self?.requestPreviousOption?() + } + self.controllerNode.updateNextEnabled = { [weak self] value in self?.navigationItem.rightBarButtonItem?.isEnabled = value } @@ -123,12 +141,12 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self?.present(c, in: .window(.root), with: a) } - if let (number, email, codeType, nextType, timeout) = self.data { + if let (number, email, codeType, nextType, timeout, hasPreviousCode, previousIsPhrase) = self.data { var appleSignInAllowed = false if case let .email(_, _, _, _, appleSignInAllowedValue, _) = codeType { appleSignInAllowed = appleSignInAllowedValue } - self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed) + self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed, hasPreviousCode: hasPreviousCode, previousIsPhrase: previousIsPhrase) } } @@ -155,6 +173,10 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self.controllerNode.animateError(text: text) } + func updateAppIsActive(_ isActive: Bool) { + self.controllerNode.updatePasteVisibility() + } + func updateNavigationItems() { guard let layout = self.validLayout, layout.size.width < 360.0 else { return @@ -168,10 +190,10 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { } } - public func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?) { + public func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?, hasPreviousCode: Bool, previousIsPhrase: Bool) { self.termsOfService = termsOfService - if self.data?.0 != number || self.data?.1 != email || self.data?.2 != codeType || self.data?.3 != nextType || self.data?.4 != timeout { - self.data = (number, email, codeType, nextType, timeout) + if self.data?.0 != number || self.data?.1 != email || self.data?.2 != codeType || self.data?.3 != nextType || self.data?.4 != timeout || self.data?.5 != hasPreviousCode || self.data?.6 != previousIsPhrase { + self.data = (number, email, codeType, nextType, timeout, hasPreviousCode, previousIsPhrase) var appleSignInAllowed = false if case let .email(_, _, _, _, appleSignInAllowedValue, _) = codeType { @@ -179,7 +201,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { } if self.isNodeLoaded { - self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed) + self.controllerNode.updateData(number: number, email: email, codeType: codeType, nextType: nextType, timeout: timeout, appleSignInAllowed: appleSignInAllowed, hasPreviousCode: hasPreviousCode, previousIsPhrase: previousIsPhrase) self.requestLayout(transition: .immediate) } } @@ -199,7 +221,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { } @objc private func nextPressed() { - guard let (_, _, type, _, _) = self.data else { + guard let (_, _, type, _, _, _, _) = self.data else { return } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift index 9bca28b0f0..d56702e7e6 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift @@ -31,6 +31,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF private let currentOptionInfoActivateAreaNode: AccessibilityAreaNode private let nextOptionTitleNode: ImmediateTextNode private let nextOptionButtonNode: HighlightableButtonNode + private let nextOptionArrowNode: ASImageNode private let resetTextNode: ImmediateTextNode private let resetNode: HighlightableButtonNode @@ -41,10 +42,15 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF private let textField: TextFieldNode private let textSeparatorNode: ASDisplayNode + private let pasteButton: HighlightableButtonNode private let codeInputView: CodeInputView private let errorTextNode: ImmediateTextNode + private let hintButtonNode: HighlightTrackingButtonNode + private let hintTextNode: ImmediateTextNode + private let hintArrowNode: ASImageNode + private var codeType: SentAuthorizationCodeType? private let countdownDisposable = MetaDisposable() @@ -53,6 +59,8 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF private var layoutArguments: (ContainerViewLayout, CGFloat)? private var appleSignInAllowed = false + private var hasPreviousCode = false + private var previousIsPhrase = false var phoneNumber: String = "" { didSet { @@ -82,6 +90,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var requestNextOption: (() -> Void)? var requestAnotherOption: (() -> Void)? + var requestPreviousOption: (() -> Void)? var updateNextEnabled: ((Bool) -> Void)? var reset: (() -> Void)? var retryReset: (() -> Void)? @@ -128,6 +137,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.titleIconNode.displaysAsynchronously = false self.currentOptionNode = ImmediateTextNodeWithEntities() + self.currentOptionNode.balancedTextLayout = true self.currentOptionNode.isUserInteractionEnabled = false self.currentOptionNode.displaysAsynchronously = false self.currentOptionNode.lineSpacing = 0.1 @@ -159,6 +169,10 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } self.nextOptionButtonNode.addSubnode(self.nextOptionTitleNode) + self.nextOptionArrowNode = ASImageNode() + self.nextOptionArrowNode.displaysAsynchronously = false + self.nextOptionArrowNode.isUserInteractionEnabled = false + self.codeInputView = CodeInputView() self.codeInputView.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance self.codeInputView.textField.returnKeyType = .done @@ -194,10 +208,25 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.textField.textField.disableAutomaticKeyboardHandling = [.forward, .backward] self.textField.textField.tintColor = self.theme.list.itemAccentColor + self.pasteButton = HighlightableButtonNode() + self.errorTextNode = ImmediateTextNode() self.errorTextNode.alpha = 0.0 self.errorTextNode.displaysAsynchronously = false self.errorTextNode.textAlignment = .center + self.errorTextNode.isUserInteractionEnabled = false + + self.hintButtonNode = HighlightableButtonNode() + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.isUserInteractionEnabled = false + + self.hintTextNode = ImmediateTextNode() + self.hintTextNode.displaysAsynchronously = false + self.hintTextNode.textAlignment = .center + self.hintTextNode.isUserInteractionEnabled = false + + self.hintArrowNode = ASImageNode() + self.hintArrowNode.displaysAsynchronously = false self.resetNode = HighlightableButtonNode() self.resetNode.displaysAsynchronously = false @@ -238,6 +267,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.addSubnode(self.codeInputView) self.addSubnode(self.textSeparatorNode) self.addSubnode(self.textField) + self.addSubnode(self.pasteButton) self.addSubnode(self.titleNode) self.addSubnode(self.titleActivateAreaNode) self.addSubnode(self.titleIconNode) @@ -245,11 +275,15 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.addSubnode(self.currentOptionActivateAreaNode) self.addSubnode(self.currentOptionInfoNode) self.addSubnode(self.nextOptionButtonNode) + self.nextOptionButtonNode.addSubnode(self.nextOptionArrowNode) self.addSubnode(self.animationNode) self.addSubnode(self.resetNode) self.addSubnode(self.resetTextNode) self.addSubnode(self.dividerNode) self.addSubnode(self.errorTextNode) + self.addSubnode(self.hintButtonNode) + self.hintButtonNode.addSubnode(self.hintTextNode) + self.hintButtonNode.addSubnode(self.hintArrowNode) self.addSubnode(self.proceedNode) self.codeInputView.updated = { [weak self] in @@ -295,6 +329,18 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } self.signInWithAppleButton?.addTarget(self, action: #selector(self.signInWithApplePressed), for: .touchUpInside) self.resetNode.addTarget(self, action: #selector(self.resetPressed), forControlEvents: .touchUpInside) + + self.pasteButton.setTitle(strings.Login_Paste, with: Font.medium(13.0), with: theme.list.itemAccentColor, for: .normal) + self.pasteButton.setBackgroundImage(generateStretchableFilledCircleImage(radius: 12.0, color: theme.list.itemAccentColor.withAlphaComponent(0.1)), for: .normal) + self.pasteButton.addTarget(self, action: #selector(self.pastePressed), forControlEvents: .touchUpInside) + + self.hintButtonNode.addTarget(self, action: #selector(self.previousOptionNodePressed), forControlEvents: .touchUpInside) + + self.hintArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: theme.list.itemAccentColor) + self.hintArrowNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.nextOptionArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: theme.list.itemAccentColor) + + self.updatePasteVisibility() } deinit { @@ -308,7 +354,19 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.view.addSubview(signInWithAppleButton) } } + + @objc private func pastePressed() { + if let text = UIPasteboard.general.string, !text.isEmpty { + if checkValidity(text: text) { + self.textField.textField.text = text + } + } + } + func updatePasteVisibility() { + self.pasteButton.isHidden = !UIPasteboard.general.hasStrings + } + func updateCode(_ code: String) { self.codeInputView.text = code self.codeChanged(text: code) @@ -343,10 +401,12 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.codeInputView.text = "" } - func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, appleSignInAllowed: Bool) { + func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, appleSignInAllowed: Bool, hasPreviousCode: Bool, previousIsPhrase: Bool) { self.codeType = codeType self.phoneNumber = number self.email = email + self.hasPreviousCode = hasPreviousCode + self.previousIsPhrase = previousIsPhrase var appleSignInAllowed = appleSignInAllowed if #available(iOS 13.0, *) { @@ -369,6 +429,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.currentOptionInfoActivateAreaNode.removeFromSupernode() } } + if let timeout = timeout { #if DEBUG let timeout = min(timeout, 5) @@ -378,7 +439,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF if let strongSelf = self { if let currentTimeoutTime = strongSelf.currentTimeoutTime, currentTimeoutTime > 0 { strongSelf.currentTimeoutTime = currentTimeoutTime - 1 - let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, timeout: strongSelf.currentTimeoutTime, strings: strongSelf.strings, primaryColor: strongSelf.theme.list.itemPrimaryTextColor, accentColor: strongSelf.theme.list.itemAccentColor) + let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, returnToCode: hasPreviousCode, timeout: strongSelf.currentTimeoutTime, strings: strongSelf.strings, primaryColor: strongSelf.theme.list.itemPrimaryTextColor, accentColor: strongSelf.theme.list.itemAccentColor) strongSelf.nextOptionTitleNode.attributedText = nextOptionText strongSelf.nextOptionButtonNode.isUserInteractionEnabled = nextOptionActive strongSelf.nextOptionButtonNode.accessibilityLabel = nextOptionText.string @@ -418,7 +479,8 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.currentTimeoutTime = nil self.countdownDisposable.set(nil) } - let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, timeout: self.currentTimeoutTime, strings: self.strings, primaryColor: self.theme.list.itemPrimaryTextColor, accentColor: self.theme.list.itemAccentColor) + + let (nextOptionText, nextOptionActive) = authorizationNextOptionText(currentType: codeType, nextType: nextType, returnToCode: hasPreviousCode, timeout: self.currentTimeoutTime, strings: self.strings, primaryColor: self.theme.list.itemPrimaryTextColor, accentColor: self.theme.list.itemAccentColor) self.nextOptionTitleNode.attributedText = nextOptionText self.nextOptionButtonNode.isUserInteractionEnabled = nextOptionActive self.nextOptionButtonNode.accessibilityLabel = nextOptionText.string @@ -427,6 +489,14 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } else { self.nextOptionButtonNode.accessibilityTraits = [.button, .notEnabled] } + + switch codeType { + case .word, .phrase: + self.nextOptionArrowNode.isHidden = !hasPreviousCode + default: + self.nextOptionArrowNode.isHidden = true + } + if let layoutArguments = self.layoutArguments { self.containerLayoutUpdated(layoutArguments.0, navigationBarHeight: layoutArguments.1, transition: .immediate) } @@ -699,7 +769,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF switch codeType { case .word, .phrase: - additionalBottomInset = 120.0 + additionalBottomInset = 100.0 self.nextOptionButtonNode.isHidden = false items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) @@ -719,13 +789,6 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.proceedNode.isHidden = true items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) } - - if case .email = codeType { - self.nextOptionButtonNode.isHidden = true - } else { - self.nextOptionButtonNode.isHidden = false - items.append(AuthorizationLayoutItem(node: self.nextOptionButtonNode, size: nextOptionSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 50.0, maxValue: 120.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) - } } } } else { @@ -740,8 +803,57 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - additionalBottomInset)), items: items, transition: transition, failIfDoesNotFit: false) + if let codeType = self.codeType { + let yOffset: CGFloat = layout.size.width > 320.0 ? 18.0 : 5.0 + if case .phrase = codeType { + self.hintButtonNode.alpha = 1.0 + self.hintButtonNode.isUserInteractionEnabled = false + + self.hintTextNode.attributedText = NSAttributedString(string: self.strings.Login_EnterPhraseHint, font: Font.regular(13.0), textColor: self.theme.list.itemSecondaryTextColor, paragraphAlignment: .center) + + let hintTextSize = self.hintTextNode.updateLayout(CGSize(width: layout.size.width - 48.0, height: .greatestFiniteMagnitude)) + transition.updateFrame(node: self.hintButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - hintTextSize.width) / 2.0), y: self.textField.frame.maxY + yOffset), size: hintTextSize)) + self.hintTextNode.frame = CGRect(origin: .zero, size: hintTextSize) + + let pasteSize = self.pasteButton.measure(layout.size) + let pasteButtonSize = CGSize(width: pasteSize.width + 16.0, height: 24.0) + transition.updateFrame(node: self.pasteButton, frame: CGRect(origin: CGPoint(x: layout.size.width - 40.0 - pasteButtonSize.width, y: self.textField.frame.midY - pasteButtonSize.height / 2.0), size: pasteButtonSize)) + self.hintArrowNode.isHidden = true + } else if case .word = codeType { + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.isUserInteractionEnabled = false + self.hintArrowNode.isHidden = true + } else if self.hasPreviousCode { + self.hintButtonNode.alpha = 1.0 + self.hintButtonNode.isUserInteractionEnabled = true + + self.hintTextNode.attributedText = NSAttributedString(string: self.previousIsPhrase ? self.strings.Login_BackToPhrase : self.strings.Login_BackToWord, font: Font.regular(13.0), textColor: self.theme.list.itemAccentColor, paragraphAlignment: .center) + + let hintTextSize = self.hintTextNode.updateLayout(CGSize(width: layout.size.width - 48.0, height: .greatestFiniteMagnitude)) + transition.updateFrame(node: self.hintButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - hintTextSize.width) / 2.0), y: self.codeInputView.frame.maxY + yOffset + 6.0), size: hintTextSize)) + self.hintTextNode.frame = CGRect(origin: .zero, size: hintTextSize) + + if let icon = self.hintArrowNode.image { + self.hintArrowNode.frame = CGRect(origin: CGPoint(x: self.hintTextNode.frame.minX - icon.size.width - 5.0, y: self.hintTextNode.frame.midY - icon.size.height / 2.0), size: icon.size) + } + self.hintArrowNode.isHidden = false + } else { + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.isUserInteractionEnabled = false + self.hintArrowNode.isHidden = true + } + } else { + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.isUserInteractionEnabled = false + self.hintArrowNode.isHidden = true + } + self.nextOptionTitleNode.frame = self.nextOptionButtonNode.bounds + if let icon = self.nextOptionArrowNode.image { + self.nextOptionArrowNode.frame = CGRect(origin: CGPoint(x: self.nextOptionTitleNode.frame.maxX + 7.0, y: self.nextOptionTitleNode.frame.midY - icon.size.height / 2.0), size: icon.size) + } + self.titleActivateAreaNode.frame = self.titleNode.frame self.currentOptionActivateAreaNode.frame = self.currentOptionNode.frame self.currentOptionInfoActivateAreaNode.frame = self.currentOptionInfoNode.frame @@ -792,12 +904,18 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } self.errorTextNode.alpha = 1.0 self.errorTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - self.errorTextNode.layer.addShakeAnimation(amplitude: -8.0, duration: 0.5, count: 6, decay: true) + + let previousHintAlpha = self.hintButtonNode.alpha + self.hintButtonNode.alpha = 0.0 + self.hintButtonNode.layer.animateAlpha(from: previousHintAlpha, to: 0.0, duration: 0.1) Queue.mainQueue().after(1.6) { self.errorTextNode.alpha = 0.0 self.errorTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + self.hintButtonNode.alpha = previousHintAlpha + self.hintButtonNode.layer.animateAlpha(from: 0.0, to: previousHintAlpha, duration: 0.1) + let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) transition.updateBackgroundColor(node: self.textSeparatorNode, color: self.theme.list.itemPlainSeparatorColor) } @@ -852,23 +970,25 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var updated = textField.text ?? "" updated.replaceSubrange(updated.index(updated.startIndex, offsetBy: range.lowerBound) ..< updated.index(updated.startIndex, offsetBy: range.upperBound), with: string) + return checkValidity(text: updated) + } + + func checkValidity(text: String) -> Bool { if let codeType = self.codeType { switch codeType { case let .word(startsWith): - if let startsWith, startsWith.count == 1, !updated.isEmpty && !updated.hasPrefix(startsWith) { + if let startsWith, startsWith.count == 1, !text.isEmpty && !text.hasPrefix(startsWith) { if self.errorTextNode.alpha.isZero { - //TODO:localize - self.animateError(text: "Incorrect, please try again") + self.animateError(text: self.strings.Login_WrongPhraseError) } return false } case let .phrase(startsWith): - if let startsWith, !updated.isEmpty { - let firstWord = updated.components(separatedBy: " ").first ?? "" + if let startsWith, !text.isEmpty { + let firstWord = text.components(separatedBy: " ").first ?? "" if !firstWord.isEmpty && !startsWith.hasPrefix(firstWord) { if self.errorTextNode.alpha.isZero { - //TODO:localize - self.animateError(text: "Incorrect, please try again") + self.animateError(text: self.strings.Login_WrongPhraseError) } return false } @@ -877,7 +997,6 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF break } } - return true } @@ -885,6 +1004,10 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.requestAnotherOption?() } + @objc func previousOptionNodePressed() { + self.requestPreviousOption?() + } + @objc func proceedPressed() { switch self.codeType { case let .fragment(url, _): diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 9b55924488..7f472c2bc5 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -45,6 +45,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth private var stateDisposable: Disposable? private let actionDisposable = MetaDisposable() + private var applicationStateDisposable: Disposable? private var didPlayPresentationAnimation = false @@ -92,6 +93,18 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth |> deliverOnMainQueue).startStrict(next: { [weak self] state in self?.updateState(state: state) }).strict() + + self.applicationStateDisposable = (self.sharedContext.applicationBindings.applicationIsActive + |> deliverOnMainQueue).start(next: { [weak self] isActive in + guard let self else { + return + } + for viewController in self.viewControllers { + if let codeController = viewController as? AuthorizationSequenceCodeEntryController { + codeController.updateAppIsActive(isActive) + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -101,6 +114,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth deinit { self.stateDisposable?.dispose() self.actionDisposable.dispose() + self.applicationStateDisposable?.dispose() } override public func loadView() { @@ -296,7 +310,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return controller } - private func codeEntryController(number: String, phoneCodeHash: String, email: String?, type: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?) -> AuthorizationSequenceCodeEntryController { + private func codeEntryController(number: String, phoneCodeHash: String, email: String?, type: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, hasPreviousCode: Bool, previousIsPhrase: Bool, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?) -> AuthorizationSequenceCodeEntryController { var currentController: AuthorizationSequenceCodeEntryController? for c in self.viewControllers { if let c = c as? AuthorizationSequenceCodeEntryController { @@ -568,6 +582,16 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } controller.requestNextOption = { [weak self, weak controller] in if let strongSelf = self { + switch type { + case .word, .phrase: + if hasPreviousCode { + strongSelf.actionDisposable.set(togglePreviousCodeEntry(account: strongSelf.account).start()) + return + } + default: + break + } + if nextType == nil { if let controller { let carrier = CTCarrier() @@ -616,11 +640,17 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } } } - controller.reset = { [weak self] in - if let strongSelf = self { - let account = strongSelf.account - let _ = strongSelf.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)).startStandalone() + controller.requestPreviousOption = { [weak self] in + guard let self else { + return } + self.actionDisposable.set(togglePreviousCodeEntry(account: self.account).start()) + } + controller.reset = { [weak self] in + guard let self else { + return + } + let _ = self.engine.auth.setState(state: UnauthorizedAccountState(isTestingEnvironment: self.account.testingEnvironment, masterDatacenterId: self.account.masterDatacenterId, contents: .empty)).startStandalone() } controller.signInWithApple = { [weak self] in guard let strongSelf = self else { @@ -645,7 +675,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth strongSelf.sharedContext.applicationBindings.openUrl(url) } } - controller.updateData(number: formatPhoneNumber(number), email: email, codeType: type, nextType: nextType, timeout: timeout, termsOfService: termsOfService) + controller.updateData(number: formatPhoneNumber(number), email: email, codeType: type, nextType: nextType, timeout: timeout, termsOfService: termsOfService, hasPreviousCode: hasPreviousCode, previousIsPhrase: previousIsPhrase) return controller } @@ -1189,12 +1219,14 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controllers.append(self.phoneEntryController(countryCode: countryCode, number: number, splashController: previousSplashController)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty && (previousSplashController == nil || self.viewControllers.count > 2)) - case let .confirmationCodeEntry(number, type, phoneCodeHash, timeout, nextType, _): + case let .confirmationCodeEntry(number, type, phoneCodeHash, timeout, nextType, _, previousCodeEntry, usePrevious): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } controllers.append(self.phoneEntryController(countryCode: AuthorizationSequenceController.defaultCountryCode(), number: "", splashController: nil)) + + var isGoingBack = false if case let .emailSetupRequired(appleSignInAllowed) = type { self.appleSignInAllowed = appleSignInAllowed controllers.append(self.emailSetupController(number: number, appleSignInAllowed: appleSignInAllowed)) @@ -1202,9 +1234,35 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if let _ = self.currentEmail { controllers.append(self.emailSetupController(number: number, appleSignInAllowed: self.appleSignInAllowed)) } - controllers.append(self.codeEntryController(number: number, phoneCodeHash: phoneCodeHash, email: self.currentEmail, type: type, nextType: nextType, timeout: timeout, termsOfService: nil)) + + if let previousCodeEntry, case let .confirmationCodeEntry(number, type, phoneCodeHash, timeout, nextType, _, _, _) = previousCodeEntry, usePrevious { + var previousIsPhrase = false + if case .phrase = type { + previousIsPhrase = true + } + controllers.append(self.codeEntryController(number: number, phoneCodeHash: phoneCodeHash, email: self.currentEmail, type: type, nextType: nextType, timeout: timeout, hasPreviousCode: true, previousIsPhrase: previousIsPhrase, termsOfService: nil)) + isGoingBack = true + } else { + var previousIsPhrase = false + if let previousCodeEntry, case let .confirmationCodeEntry(_, type, _, _, _, _, _, _) = previousCodeEntry { + if case .phrase = type { + previousIsPhrase = true + } + } + controllers.append(self.codeEntryController(number: number, phoneCodeHash: phoneCodeHash, email: self.currentEmail, type: type, nextType: nextType, timeout: timeout, hasPreviousCode: previousCodeEntry != nil, previousIsPhrase: previousIsPhrase, termsOfService: nil)) + } + } + + if isGoingBack, let currentLastController = self.viewControllers.last as? AuthorizationSequenceCodeEntryController, !currentLastController.isWordOrPhrase { + var tempControllers = controllers + tempControllers.append(currentLastController) + self.setViewControllers(tempControllers, animated: false) + Queue.mainQueue().justDispatch { + self.setViewControllers(controllers, animated: true) + } + } else { + self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) } - self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) case let .passwordEntry(hint, _, _, suggestReset, syncContacts): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { diff --git a/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift b/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift index 95d94bd958..a5e86a01e4 100644 --- a/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift +++ b/submodules/AuthorizationUtils/Sources/AuthorizationOptionText.swift @@ -56,9 +56,18 @@ public func authorizationCurrentOptionText(_ type: SentAuthorizationCodeType, ph } } -public func authorizationNextOptionText(currentType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, strings: PresentationStrings, primaryColor: UIColor, accentColor: UIColor) -> (NSAttributedString, Bool) { +public func authorizationNextOptionText(currentType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, returnToCode: Bool = false, timeout: Int32?, strings: PresentationStrings, primaryColor: UIColor, accentColor: UIColor) -> (NSAttributedString, Bool) { let font = Font.regular(16.0) + switch currentType { + case .word, .phrase: + if returnToCode { + return (NSAttributedString(string: strings.Login_ReturnToCode, font: font, textColor: accentColor, paragraphAlignment: .center), true) + } + default: + break + } + if let nextType = nextType, let timeout = timeout, timeout > 0 { let minutes = timeout / 60 let seconds = timeout % 60 diff --git a/submodules/Display/Source/ActionSheetTextItem.swift b/submodules/Display/Source/ActionSheetTextItem.swift index b13d4f3240..7921a5fe45 100644 --- a/submodules/Display/Source/ActionSheetTextItem.swift +++ b/submodules/Display/Source/ActionSheetTextItem.swift @@ -4,11 +4,18 @@ import AsyncDisplayKit import Markdown public class ActionSheetTextItem: ActionSheetItem { + public enum Font { + case `default` + case large + } + public let title: String + public let font: Font public let parseMarkdown: Bool - public init(title: String, parseMarkdown: Bool = true) { + public init(title: String, font: Font = .default, parseMarkdown: Bool = true) { self.title = title + self.font = font self.parseMarkdown = parseMarkdown } @@ -30,8 +37,6 @@ public class ActionSheetTextItem: ActionSheetItem { } public class ActionSheetTextNode: ActionSheetItemNode { - private let defaultFont: UIFont - private let theme: ActionSheetControllerTheme private var item: ActionSheetTextItem? @@ -42,7 +47,6 @@ public class ActionSheetTextNode: ActionSheetItemNode { override public init(theme: ActionSheetControllerTheme) { self.theme = theme - self.defaultFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) self.label = ImmediateTextNode() self.label.isUserInteractionEnabled = false @@ -66,8 +70,16 @@ public class ActionSheetTextNode: ActionSheetItemNode { func setItem(_ item: ActionSheetTextItem) { self.item = item - let defaultFont = Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) - let boldFont = Font.semibold(floor(theme.baseFontSize * 13.0 / 17.0)) + let fontSize: CGFloat + switch item.font { + case .default: + fontSize = 13.0 + case .large: + fontSize = 15.0 + } + + let defaultFont = Font.regular(floor(self.theme.baseFontSize * fontSize / 17.0)) + let boldFont = Font.semibold(floor(self.theme.baseFontSize * fontSize / 17.0)) if item.parseMarkdown { let body = MarkdownAttributeSet(font: defaultFont, textColor: self.theme.secondaryTextColor) diff --git a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift index 352e0c9815..7b98aec08e 100644 --- a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift +++ b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift @@ -68,14 +68,18 @@ public final class LiveLocationManagerImpl: LiveLocationManager { for media in message.media { if let telegramMap = media as? TelegramMediaMap { if let liveBroadcastingTimeout = telegramMap.liveBroadcastingTimeout { - if message.timestamp + liveBroadcastingTimeout > timestamp { + if liveBroadcastingTimeout == liveLocationIndefinitePeriod || message.timestamp + liveBroadcastingTimeout > timestamp { activeLiveBroadcastingTimeout = liveBroadcastingTimeout } } } } if let activeLiveBroadcastingTimeout = activeLiveBroadcastingTimeout { - broadcastToMessageIds[message.id] = message.timestamp + activeLiveBroadcastingTimeout + if activeLiveBroadcastingTimeout == liveLocationIndefinitePeriod { + broadcastToMessageIds[message.id] = activeLiveBroadcastingTimeout + } else { + broadcastToMessageIds[message.id] = message.timestamp + activeLiveBroadcastingTimeout + } } else { stopMessageIds.insert(message.id) } diff --git a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift index 0039970146..5ec0d8be44 100644 --- a/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift +++ b/submodules/LiveLocationTimerNode/Sources/ChatMessageLiveLocationTimerNode.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData +private let infinityFont = Font.with(size: 15.0, design: .round, weight: .bold) private let textFont = Font.with(size: 13.0, design: .round, weight: .bold) private let smallTextFont = Font.with(size: 11.0, design: .round, weight: .bold) @@ -72,14 +73,19 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { let remaining = beginTimestamp + timeout - timestamp value = CGFloat(max(0.0, 1.0 - min(1.0, remaining / timeout))) - let intRemaining = Int32(remaining) let string: String - if intRemaining > 60 * 60 { - let hours = Int32(round(remaining / (60.0 * 60.0))) - string = strings.Map_LiveLocationShortHour("\(hours)").string + if timeout < 0.0 { + value = 0.0 + string = "∞" } else { - let minutes = Int32(round(remaining / (60.0))) - string = "\(minutes)" + let intRemaining = Int32(remaining) + if intRemaining > 60 * 60 { + let hours = Int32(round(remaining / (60.0 * 60.0))) + string = strings.Map_LiveLocationShortHour("\(hours)").string + } else { + let minutes = Int32(round(remaining / (60.0))) + string = "\(minutes)" + } } return ChatMessageLiveLocationTimerNodeParams(backgroundColor: backgroundColor, foregroundColor: foregroundColor, textColor: textColor, value: value, string: string) @@ -120,16 +126,27 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode { path.lineCapStyle = .round path.stroke() - let attributes: [NSAttributedString.Key: Any] = [.font: parameters.string.count > 2 ? smallTextFont : textFont, .foregroundColor: parameters.foregroundColor] + let font: UIFont + if parameters.string == "∞" { + font = infinityFont + } else if parameters.string.count > 2 { + font = smallTextFont + } else { + font = textFont + } + + let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: parameters.foregroundColor] let nsString = parameters.string as NSString let size = nsString.size(withAttributes: attributes) - var offset: CGFloat = 0.0 - if parameters.string.count > 2 { - offset = UIScreenPixel + var offset = CGPoint() + if parameters.string == "∞" { + offset = CGPoint(x: 1.0, y: -1.0) + } else if parameters.string.count > 2 { + offset = CGPoint(x: 0.0, y: UIScreenPixel) } - nsString.draw(at: CGPoint(x: floor((bounds.size.width - size.width) / 2.0), y: floor((bounds.size.height - size.height) / 2.0) + offset), withAttributes: attributes) + nsString.draw(at: CGPoint(x: floor((bounds.size.width - size.width) / 2.0) + offset.x, y: floor((bounds.size.height - size.height) / 2.0) + offset.y), withAttributes: attributes) } } } diff --git a/submodules/LiveLocationTimerNode/Sources/LiveLocationWavesNode.swift b/submodules/LiveLocationTimerNode/Sources/LiveLocationWavesNode.swift index 43390801fd..6521f9ae6d 100644 --- a/submodules/LiveLocationTimerNode/Sources/LiveLocationWavesNode.swift +++ b/submodules/LiveLocationTimerNode/Sources/LiveLocationWavesNode.swift @@ -5,10 +5,12 @@ import AsyncDisplayKit private final class LiveLocationWavesNodeParams: NSObject { let color: UIColor + let extend: Bool let progress: CGFloat - init(color: UIColor, progress: CGFloat) { + init(color: UIColor, extend: Bool, progress: CGFloat) { self.color = color + self.extend = extend self.progress = progress super.init() @@ -26,6 +28,12 @@ public final class LiveLocationWavesNode: ASDisplayNode { } } + public var extend: Bool { + didSet { + self.setNeedsDisplay() + } + } + private var effectiveProgress: CGFloat = 0.0 { didSet { self.setNeedsDisplay() @@ -34,8 +42,9 @@ public final class LiveLocationWavesNode: ASDisplayNode { var animator: ConstantDisplayLinkAnimator? - public init(color: UIColor) { + public init(color: UIColor, extend: Bool = false) { self.color = color + self.extend = extend super.init() @@ -105,7 +114,7 @@ public final class LiveLocationWavesNode: ASDisplayNode { public override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { let t = CACurrentMediaTime() let value: CGFloat = CGFloat(t.truncatingRemainder(dividingBy: 2.0)) / 2.0 - return LiveLocationWavesNodeParams(color: self.color, progress: value) + return LiveLocationWavesNodeParams(color: self.color, extend: self.extend, progress: value) } @objc public override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -142,7 +151,9 @@ public final class LiveLocationWavesNode: ASDisplayNode { } context.setAlpha(alpha * 0.7) - draw(context, position, false) + if !parameters.extend { + draw(context, position, false) + } draw(context, position, true) var progress = parameters.progress + 0.5 @@ -157,7 +168,9 @@ public final class LiveLocationWavesNode: ASDisplayNode { } context.setAlpha(largerAlpha * 0.7) - draw(context, largerPos, false) + if !parameters.extend { + draw(context, largerPos, false) + } draw(context, largerPos, true) } } diff --git a/submodules/LocationUI/Sources/LocationActionListItem.swift b/submodules/LocationUI/Sources/LocationActionListItem.swift index aa5ead7449..9d8ddf9b8e 100644 --- a/submodules/LocationUI/Sources/LocationActionListItem.swift +++ b/submodules/LocationUI/Sources/LocationActionListItem.swift @@ -15,6 +15,7 @@ public enum LocationActionListItemIcon: Equatable { case location case liveLocation case stopLiveLocation + case extendLiveLocation case venue(TelegramMediaMap) public static func ==(lhs: LocationActionListItemIcon, rhs: LocationActionListItemIcon) -> Bool { @@ -37,6 +38,12 @@ public enum LocationActionListItemIcon: Equatable { } else { return false } + case .extendLiveLocation: + if case .extendLiveLocation = rhs { + return true + } else { + return false + } case let .venue(lhsVenue): if case let .venue(rhsVenue) = rhs, lhsVenue.venue?.id == rhsVenue.venue?.id { return true @@ -63,18 +70,67 @@ private func generateLocationIcon(theme: PresentationTheme) -> UIImage { })! } -private func generateLiveLocationIcon(theme: PresentationTheme, stop: Bool) -> UIImage { +private enum LiveLocationIconType { + case start + case stop + case extend +} +private func generateLiveLocationIcon(theme: PresentationTheme, type: LiveLocationIconType) -> UIImage { return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + let imageName: String + let color: UIColor + switch type { + case .start: + imageName = "Location/SendLiveLocationIcon" + color = UIColor(rgb: 0x6cc139) + case .stop: + imageName = "Location/SendLocationIcon" + color = UIColor(rgb: 0xff6464) + case .extend: + imageName = "Location/SendLocationIcon" + color = UIColor(rgb: 0x6cc139) + } + context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(rgb: stop ? 0xff6464 : 0x6cc139).cgColor) + context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.translateBy(x: size.width / 2.0, y: size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - if let image = generateTintedImage(image: UIImage(bundleImageName: stop ? "Location/SendLocationIcon" : "Location/SendLiveLocationIcon"), color: theme.chat.inputPanel.actionControlForegroundColor) { + if let image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.chat.inputPanel.actionControlForegroundColor) { context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + + if case .extend = type { + context.setStrokeColor(theme.chat.inputPanel.actionControlForegroundColor.cgColor) + context.setLineWidth(2.0 - UIScreenPixel) + context.setLineCap(.round) + + let length: CGFloat = 6.0 + UIScreenPixel + context.move(to: CGPoint(x: 8.0 + 0.0, y: image.size.height / 2.0)) + context.addLine(to: CGPoint(x: 8.0 + length, y: image.size.height / 2.0)) + context.strokePath() + + context.move(to: CGPoint(x: 8.0 + length / 2.0, y: image.size.height / 2.0 - length / 2.0)) + context.addLine(to: CGPoint(x: 8.0 + length / 2.0, y: image.size.height / 2.0 + length / 2.0)) + context.strokePath() + } else if case .stop = type { + context.setStrokeColor(color.cgColor) + context.setLineWidth(5.0) + + context.move(to: CGPoint(x: 10.0, y: size.height - 10.0)) + context.addLine(to: CGPoint(x: size.width - 10.0, y: 10.0)) + context.strokePath() + + context.setStrokeColor(theme.chat.inputPanel.actionControlForegroundColor.cgColor) + context.setLineWidth(2.0 - UIScreenPixel) + context.setLineCap(.round) + + context.move(to: CGPoint(x: 12.0, y: size.height - 12.0)) + context.addLine(to: CGPoint(x: size.width - 12.0, y: 12.0)) + context.strokePath() + } } })! } @@ -273,10 +329,18 @@ final class LocationActionListItemNode: ListViewItemNode { strongSelf.iconNode.isHidden = false strongSelf.venueIconNode.isHidden = true strongSelf.iconNode.image = generateLocationIcon(theme: item.presentationData.theme) - case .liveLocation, .stopLiveLocation: + case .liveLocation: strongSelf.iconNode.isHidden = false strongSelf.venueIconNode.isHidden = true - strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, stop: updatedIcon == .stopLiveLocation) + strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, type: .start) + case .stopLiveLocation: + strongSelf.iconNode.isHidden = false + strongSelf.venueIconNode.isHidden = true + strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, type: .stop) + case .extendLiveLocation: + strongSelf.iconNode.isHidden = false + strongSelf.venueIconNode.isHidden = true + strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, type: .extend) case let .venue(venue): strongSelf.iconNode.isHidden = true strongSelf.venueIconNode.isHidden = false @@ -293,13 +357,14 @@ final class LocationActionListItemNode: ListViewItemNode { strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: type ?? "", flag: flag, background: true)) } - if updatedIcon == .stopLiveLocation { - let wavesNode = LiveLocationWavesNode(color: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor) + switch updatedIcon { + case .stopLiveLocation, .extendLiveLocation: + let wavesNode = LiveLocationWavesNode(color: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor, extend: updatedIcon == .extendLiveLocation) strongSelf.addSubnode(wavesNode) strongSelf.wavesNode = wavesNode - } else if let wavesNode = strongSelf.wavesNode { + default: + strongSelf.wavesNode?.removeFromSupernode() strongSelf.wavesNode = nil - wavesNode.removeFromSupernode() } strongSelf.wavesNode?.color = item.presentationData.theme.chat.inputPanel.actionControlForegroundColor } @@ -349,7 +414,7 @@ final class LocationActionListItemNode: ListViewItemNode { strongSelf.timerNode = timerNode } let timerSize = CGSize(width: 28.0, height: 28.0) - timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: beginTimestamp, timeout: timeout, strings: item.presentationData.strings) + timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: beginTimestamp, timeout: Int32(timeout) == liveLocationIndefinitePeriod ? -1.0 : timeout, strings: item.presentationData.strings) timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 6d73920913..9e2ee47237 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -142,33 +142,34 @@ public final class LocationPickerController: ViewController, AttachmentContainab return } let controller = ActionSheetController(presentationData: strongSelf.presentationData) - var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription + var title = strongSelf.presentationData.strings.Map_LiveLocationGroupNewDescription if case let .share(peer, _, _) = strongSelf.mode, let peer = peer, case .user = peer { - title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(peer.compactDisplayTitle).string + title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(peer.compactDisplayTitle).string } + + let sendLiveLocationImpl: (Int32) -> Void = { [weak self, weak controller] period in + controller?.dismissAnimated() + guard let self, let controller else { + return + } + controller.dismissAnimated() + self.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period), nil, nil, nil, nil) + self.dismiss() + } + controller.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: title), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: { [weak self, weak controller] in - controller?.dismissAnimated() - if let strongSelf = self { - strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 15 * 60), nil, nil, nil, nil) - strongSelf.dismiss() - } + ActionSheetTextItem(title: title, font: .large, parseMarkdown: true), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForMinutes(15), color: .accent, action: { sendLiveLocationImpl(15 * 60) }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor1Hour, color: .accent, action: { [weak self, weak controller] in - controller?.dismissAnimated() - if let strongSelf = self { - strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 60 * 60 - 1), nil, nil, nil, nil) - strongSelf.dismiss() - } + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(1), color: .accent, action: { + sendLiveLocationImpl(60 * 60 - 1) }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor8Hours, color: .accent, action: { [weak self, weak controller] in - controller?.dismissAnimated() - if let strongSelf = self { - strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 8 * 60 * 60), nil, nil, nil, nil) - strongSelf.dismiss() - } + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(8), color: .accent, action: { + sendLiveLocationImpl(8 * 60 * 60) + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationIndefinite, color: .accent, action: { + sendLiveLocationImpl(liveLocationIndefinitePeriod) }) ]), ActionSheetItemGroup(items: [ diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 71fd3a7f87..4d3d40dfc8 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -47,12 +47,12 @@ class LocationViewInteraction { let share: () -> Void let setupProximityNotification: (Bool, EngineMessage.Id?) -> Void let updateSendActionHighlight: (Bool) -> Void - let sendLiveLocation: (Int32?) -> Void + let sendLiveLocation: (Int32?, Bool) -> Void let stopLiveLocation: () -> Void let updateRightBarButton: (LocationViewRightBarButton) -> Void let present: (ViewController) -> Void - init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) { + init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) { self.toggleMapModeSelection = toggleMapModeSelection self.updateMapMode = updateMapMode self.toggleTrackingMode = toggleTrackingMode @@ -323,7 +323,7 @@ public final class LocationViewController: ViewController { } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: updatedPresentationData, title: strongSelf.presentationData.strings.Location_LiveLocationRequired_Title, text: strongSelf.presentationData.strings.Location_LiveLocationRequired_Description, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Location_LiveLocationRequired_ShareLocation, action: { completion() - strongSelf.interaction?.sendLiveLocation(distance) + strongSelf.interaction?.sendLiveLocation(distance, false) }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})], actionLayout: .vertical), in: .window(.root)) } completion() @@ -341,7 +341,7 @@ public final class LocationViewController: ViewController { return } strongSelf.controllerNode.updateSendActionHighlight(highlighted) - }, sendLiveLocation: { [weak self] distance in + }, sendLiveLocation: { [weak self] distance, extend in guard let strongSelf = self else { return } @@ -400,9 +400,14 @@ public final class LocationViewController: ViewController { let _ = (context.account.postbox.loadedPeerWithId(subject.id.peerId) |> deliverOnMainQueue).start(next: { peer in let controller = ActionSheetController(presentationData: strongSelf.presentationData) - var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription - if let user = peer as? TelegramUser { - title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(EnginePeer(user).compactDisplayTitle).string + var title: String + if extend { + title = strongSelf.presentationData.strings.Map_LiveLocationExtendDescription + } else { + title = strongSelf.presentationData.strings.Map_LiveLocationGroupNewDescription + if let user = peer as? TelegramUser { + title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(EnginePeer(user).compactDisplayTitle).string + } } let sendLiveLocationImpl: (Int32) -> Void = { [weak controller] period in @@ -418,15 +423,18 @@ public final class LocationViewController: ViewController { controller.setItemGroups([ ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: title), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: { + ActionSheetTextItem(title: title, font: .large, parseMarkdown: true), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForMinutes(15), color: .accent, action: { sendLiveLocationImpl(15 * 60) }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor1Hour, color: .accent, action: { + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(1), color: .accent, action: { sendLiveLocationImpl(60 * 60 - 1) }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor8Hours, color: .accent, action: { + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationForHours(8), color: .accent, action: { sendLiveLocationImpl(8 * 60 * 60) + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationIndefinite, color: .accent, action: { + sendLiveLocationImpl(liveLocationIndefinitePeriod) }) ]), ActionSheetItemGroup(items: [ diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 9f13a79efd..9ee4346514 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -42,21 +42,21 @@ private struct LocationViewTransaction { private enum LocationViewEntryId: Hashable { case info - case toggleLiveLocation + case toggleLiveLocation(Bool) case liveLocation(UInt32) } private enum LocationViewEntry: Comparable, Identifiable { case info(PresentationTheme, TelegramMediaMap, String?, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Bool) - case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?) + case toggleLiveLocation(PresentationTheme, String, String, Double?, Double?, Bool) case liveLocation(PresentationTheme, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessage, Double?, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime, Int) var stableId: LocationViewEntryId { switch self { case .info: return .info - case .toggleLiveLocation: - return .toggleLiveLocation + case let .toggleLiveLocation(_, _, _, _, _, additional): + return .toggleLiveLocation(additional) case let .liveLocation(_, _, _, message, _, _, _, _, _): return .liveLocation(message.stableId) } @@ -70,8 +70,8 @@ private enum LocationViewEntry: Comparable, Identifiable { } else { return false } - case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsBeginTimestamp, lhsTimeout): - if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsBeginTimestamp, rhsTimeout) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsBeginTimestamp == rhsBeginTimestamp, lhsTimeout == rhsTimeout { + case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsBeginTimestamp, lhsTimeout, lhsAdditional): + if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsBeginTimestamp, rhsTimeout, rhsAdditional) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsBeginTimestamp == rhsBeginTimestamp, lhsTimeout == rhsTimeout, lhsAdditional == rhsAdditional { return true } else { return false @@ -94,10 +94,12 @@ private enum LocationViewEntry: Comparable, Identifiable { case .toggleLiveLocation, .liveLocation: return true } - case .toggleLiveLocation: + case let .toggleLiveLocation(_, _, _, _, _, lhsAdditional): switch rhs { - case .info, .toggleLiveLocation: + case .info: return false + case let .toggleLiveLocation(_, _, _, _, _, rhsAdditional): + return !lhsAdditional && rhsAdditional case .liveLocation: return true } @@ -135,18 +137,36 @@ private enum LocationViewEntry: Comparable, Identifiable { }, walkingAction: { interaction?.requestDirections(location, nil, .walking) }) - case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout): - let beginTimeAndTimeout: (Double, Double)? + case let .toggleLiveLocation(_, title, subtitle, beginTimstamp, timeout, additional): + var beginTimeAndTimeout: (Double, Double)? if let beginTimstamp = beginTimstamp, let timeout = timeout { beginTimeAndTimeout = (beginTimstamp, timeout) } else { beginTimeAndTimeout = nil } - return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: beginTimeAndTimeout != nil ? .stopLiveLocation : .liveLocation, beginTimeAndTimeout: beginTimeAndTimeout, action: { + + let icon: LocationActionListItemIcon + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod, !additional { + icon = .extendLiveLocation + } else if beginTimeAndTimeout != nil { + icon = .stopLiveLocation + } else { + icon = .liveLocation + } + + return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: context.engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: !additional ? beginTimeAndTimeout : nil, action: { if beginTimeAndTimeout != nil { - interaction?.stopLiveLocation() + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { + if additional { + interaction?.stopLiveLocation() + } else { + interaction?.sendLiveLocation(nil, true) + } + } else { + interaction?.stopLiveLocation() + } } else { - interaction?.sendLiveLocation(nil) + interaction?.sendLiveLocation(nil, false) } }, highlighted: { highlight in interaction?.updateSendActionHighlight(highlight) @@ -421,7 +441,12 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan if case let .channel(channel) = subject.author, case .broadcast = channel.info, activeOwnLiveLocation == nil { } else { - entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, beginTime, timeout)) + if let timeout, Int32(timeout) != liveLocationIndefinitePeriod { + entries.append(.toggleLiveLocation(presentationData.theme, presentationData.strings.Map_SharingLocation, presentationData.strings.Map_TapToAddTime, beginTime, timeout, false)) + entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, beginTime, timeout, true)) + } else { + entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, beginTime, timeout, false)) + } } var sortedLiveLocations: [EngineMessage] = [] @@ -452,7 +477,12 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan if let timeout = location.liveBroadcastingTimeout { liveBroadcastingTimeout = timeout } - let remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime) + let remainingTime: Int32 + if liveBroadcastingTimeout == liveLocationIndefinitePeriod { + remainingTime = liveLocationIndefinitePeriod + } else { + remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime) + } if message.flags.contains(.Incoming) && remainingTime != 0 && proximityNotification == nil { proximityNotification = false } diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift index 45dfe6526c..6ff6799495 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberController.swift @@ -108,7 +108,7 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll codeController.openFragment = { url in context.sharedContext.applicationBindings.openUrl(url) } - codeController.updateData(number: formatPhoneNumber(context: context, number: phoneNumber), email: nil, codeType: next.type, nextType: nil, timeout: next.timeout, termsOfService: nil) + codeController.updateData(number: formatPhoneNumber(context: context, number: phoneNumber), email: nil, codeType: next.type, nextType: nil, timeout: next.timeout, termsOfService: nil, hasPreviousCode: false, previousIsPhrase: false) dismissImpl = { [weak codeController] in codeController?.dismiss() } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 19ec37b50b..14ef94ac64 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -1446,7 +1446,7 @@ public func privacyAndSecurityController( emailChangeCompletion(codeController) })) } - codeController.updateData(number: "", email: email, codeType: .email(emailPattern: "", length: data.length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), nextType: nil, timeout: nil, termsOfService: nil) + codeController.updateData(number: "", email: email, codeType: .email(emailPattern: "", length: data.length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), nextType: nil, timeout: nil, termsOfService: nil, hasPreviousCode: false, previousIsPhrase: false) pushControllerImpl?(codeController, true) dismissCodeControllerImpl = { [weak codeController] in codeController?.dismiss() diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 3eb5474c0f..e2cf3449f8 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -306,7 +306,20 @@ public func sendAuthorizationCode(accountManager: AccountManager mapToSignal { success -> Signal in if success { return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + var previousCodeEntry: UnauthorizedAccountStateContents? + if let state = transaction.getState() as? UnauthorizedAccountState, case let .confirmationCodeEntry(_, type, _, _, _, _, previousCodeEntryValue, _) = state.contents { + if let previousCodeEntryValue { + previousCodeEntry = previousCodeEntryValue + } else { + switch type { + case .word, .phrase: + previousCodeEntry = state.contents + default: + break + } + } + } + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .sentCode(account) } @@ -318,7 +331,20 @@ public func sendAuthorizationCode(accountManager: AccountManager mapToSignal { success -> Signal in if success { return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + var previousCodeEntry: UnauthorizedAccountStateContents? + if let state = transaction.getState() as? UnauthorizedAccountState, case let .confirmationCodeEntry(_, type, _, _, _, _, previousCodeEntryValue, _) = state.contents { + if let previousCodeEntryValue { + previousCodeEntry = previousCodeEntryValue + } else { + switch type { + case .word, .phrase: + previousCodeEntry = state.contents + default: + break + } + } + } + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .sentCode(account) } @@ -427,7 +466,20 @@ private func internalResendAuthorizationCode(accountManager: AccountManager Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(number, _, hash, _, nextType, syncContacts): + case let .confirmationCodeEntry(number, type, hash, _, nextType, syncContacts, previousCodeEntryValue, _): if nextType != nil { + var previousCodeEntry: UnauthorizedAccountStateContents? + if let previousCodeEntryValue { + previousCodeEntry = previousCodeEntryValue + } else { + switch type { + case .word, .phrase: + previousCodeEntry = state.contents + default: + break + } + } return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false) |> mapError { error -> AuthorizationCodeRequestError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { @@ -503,7 +566,7 @@ public func resendAuthorizationCode(accountManager: AccountManager mapToSignal { success -> Signal in if success { return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .sentCode(account) } @@ -516,7 +579,7 @@ public func resendAuthorizationCode(accountManager: AccountManager map { _ -> Void in return Void() } } - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) case .sentCodeSuccess: break } @@ -675,7 +738,7 @@ public func sendLoginEmailCode(account: UnauthorizedAccount, email: String) -> S return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts): + case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts, _, _): return account.network.request(Api.functions.account.sendVerifyEmailCode(purpose: .emailVerifyPurposeLoginSetup(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash), email: email), automaticFloodWait: false) |> `catch` { error -> Signal in let errorDescription = error.errorDescription ?? "" @@ -694,8 +757,8 @@ public func sendLoginEmailCode(account: UnauthorizedAccount, email: String) -> S |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Signal in switch result { - case let .sentEmailCode(emailPattern, length): - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: .email(emailPattern: emailPattern, length: length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), hash: phoneCodeHash, timeout: nil, nextType: nil, syncContacts: syncContacts))) + case let .sentEmailCode(emailPattern, length): + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: .email(emailPattern: emailPattern, length: length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), hash: phoneCodeHash, timeout: nil, nextType: nil, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) } return .complete() } @@ -754,7 +817,7 @@ public func verifyLoginEmailSetup(account: UnauthorizedAccount, code: Authorizat return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts): + case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts, _, _): let verification: Api.EmailVerification switch code { case let .emailCode(code): @@ -793,7 +856,7 @@ public func verifyLoginEmailSetup(account: UnauthorizedAccount, code: Authorizat parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType, syncContacts: syncContacts))) + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) case .sentCodeSuccess: break } @@ -831,7 +894,7 @@ public func resetLoginEmail(account: UnauthorizedAccount, phoneNumber: String, p return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts): + case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts, _, _): return account.network.request(Api.functions.auth.resetLoginEmail(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash), automaticFloodWait: false) |> `catch` { error -> Signal in let errorDescription = error.errorDescription ?? "" @@ -854,7 +917,7 @@ public func resetLoginEmail(account: UnauthorizedAccount, phoneNumber: String, p parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts))) + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) return .complete() case .sentCodeSuccess: @@ -883,7 +946,7 @@ public func authorizeWithCode(accountManager: AccountManager Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { - case let .confirmationCodeEntry(number, _, hash, _, _, syncContacts): + case let .confirmationCodeEntry(number, _, hash, _, _, syncContacts, _, _): var flags: Int32 = 0 var phoneCode: String? var emailVerification: Api.EmailVerification? @@ -1326,6 +1389,17 @@ public func resetAuthorizationState(account: UnauthorizedAccount, to value: Auth } } +public func togglePreviousCodeEntry(account: UnauthorizedAccount) -> Signal { + return account.postbox.transaction { transaction -> Void in + if let state = transaction.getState() as? UnauthorizedAccountState { + if case let .confirmationCodeEntry(number, type, hash, timeout, nextType, syncContacts, previousCodeEntry, usePrevious) = state.contents, let previousCodeEntry { + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: state.isTestingEnvironment, masterDatacenterId: state.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: type, hash: hash, timeout: timeout, nextType: nextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: !usePrevious))) + } + } + } + |> ignoreValues +} + public enum ReportMissingCodeError { case generic } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift index e4fa801154..2039d62d8b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift @@ -1,5 +1,7 @@ import Postbox +public let liveLocationIndefinitePeriod: Int32 = 0x7fffffff + public final class NamedGeoPlace: PostboxCoding, Equatable { public let country: String? public let state: String? diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift index 7d2625685b..9bab8302f8 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift @@ -173,10 +173,10 @@ public struct UnauthorizedAccountTermsOfService: PostboxCoding, Equatable { } } -public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { +public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { case empty case phoneEntry(countryCode: Int32, number: String) - case confirmationCodeEntry(number: String, type: SentAuthorizationCodeType, hash: String, timeout: Int32?, nextType: AuthorizationCodeNextType?, syncContacts: Bool) + case confirmationCodeEntry(number: String, type: SentAuthorizationCodeType, hash: String, timeout: Int32?, nextType: AuthorizationCodeNextType?, syncContacts: Bool, previousCodeEntry: UnauthorizedAccountStateContents?, usePrevious: Bool) case passwordEntry(hint: String, number: String?, code: AuthorizationCode?, suggestReset: Bool, syncContacts: Bool) case passwordRecovery(hint: String, number: String?, code: AuthorizationCode?, emailPattern: String, syncContacts: Bool) case awaitingAccountReset(protectedUntil: Int32, number: String?, syncContacts: Bool) @@ -193,7 +193,7 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { if let value = decoder.decodeOptionalInt32ForKey("nt") { nextType = AuthorizationCodeNextType(rawValue: value) } - self = .confirmationCodeEntry(number: decoder.decodeStringForKey("num", orElse: ""), type: decoder.decodeObjectForKey("t", decoder: { SentAuthorizationCodeType(decoder: $0) }) as! SentAuthorizationCodeType, hash: decoder.decodeStringForKey("h", orElse: ""), timeout: decoder.decodeOptionalInt32ForKey("tm"), nextType: nextType, syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) + self = .confirmationCodeEntry(number: decoder.decodeStringForKey("num", orElse: ""), type: decoder.decodeObjectForKey("t", decoder: { SentAuthorizationCodeType(decoder: $0) }) as! SentAuthorizationCodeType, hash: decoder.decodeStringForKey("h", orElse: ""), timeout: decoder.decodeOptionalInt32ForKey("tm"), nextType: nextType, syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0, previousCodeEntry: decoder.decodeObjectForKey("previousCodeEntry", decoder: { UnauthorizedAccountStateContents(decoder: $0) }) as? UnauthorizedAccountStateContents, usePrevious: decoder.decodeInt32ForKey("usePrevious", orElse: 1) != 0) case UnauthorizedAccountStateContentsValue.passwordEntry.rawValue: var code: AuthorizationCode? if let modernCode = decoder.decodeObjectForKey("modernCode", decoder: { AuthorizationCode(decoder: $0) }) as? AuthorizationCode { @@ -228,7 +228,7 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { encoder.encodeInt32(UnauthorizedAccountStateContentsValue.phoneEntry.rawValue, forKey: "v") encoder.encodeInt32(countryCode, forKey: "cc") encoder.encodeString(number, forKey: "n") - case let .confirmationCodeEntry(number, type, hash, timeout, nextType, syncContacts): + case let .confirmationCodeEntry(number, type, hash, timeout, nextType, syncContacts, previousCodeEntry, usePrevious): encoder.encodeInt32(UnauthorizedAccountStateContentsValue.confirmationCodeEntry.rawValue, forKey: "v") encoder.encodeString(number, forKey: "num") encoder.encodeObject(type, forKey: "t") @@ -244,6 +244,14 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { encoder.encodeNil(forKey: "nt") } encoder.encodeInt32(syncContacts ? 1 : 0, forKey: "syncContacts") + + if let previousCodeEntry = previousCodeEntry { + encoder.encodeObject(previousCodeEntry, forKey: "previousCodeEntry") + encoder.encodeInt32(usePrevious ? 1 : 0, forKey: "usePrevious") + } else { + encoder.encodeNil(forKey: "previousCodeEntry") + encoder.encodeInt32(0, forKey: "usePrevious") + } case let .passwordEntry(hint, number, code, suggestReset, syncContacts): encoder.encodeInt32(UnauthorizedAccountStateContentsValue.passwordEntry.rawValue, forKey: "v") encoder.encodeString(hint, forKey: "h") @@ -312,8 +320,8 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { } else { return false } - case let .confirmationCodeEntry(lhsNumber, lhsType, lhsHash, lhsTimeout, lhsNextType, lhsSyncContacts): - if case let .confirmationCodeEntry(rhsNumber, rhsType, rhsHash, rhsTimeout, rhsNextType, rhsSyncContacts) = rhs { + case let .confirmationCodeEntry(lhsNumber, lhsType, lhsHash, lhsTimeout, lhsNextType, lhsSyncContacts, lhsPreviousCodeEntry, lhsUsePrevious): + if case let .confirmationCodeEntry(rhsNumber, rhsType, rhsHash, rhsTimeout, rhsNextType, rhsSyncContacts, rhsPreviousCodeEntry, rhsUsePrevious) = rhs { if lhsNumber != rhsNumber { return false } @@ -332,6 +340,12 @@ public enum UnauthorizedAccountStateContents: PostboxCoding, Equatable { if lhsSyncContacts != rhsSyncContacts { return false } + if lhsPreviousCodeEntry != rhsPreviousCodeEntry { + return false + } + if lhsUsePrevious != rhsUsePrevious { + return false + } return true } else { return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift index 7c8255b7ec..0791de33fd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PeerLiveLocationsContext.swift @@ -19,7 +19,7 @@ func _internal_topPeerActiveLiveLocationMessages(viewTracker: AccountViewTracker for entry in view.entries { for media in entry.message.media { if let location = media as? TelegramMediaMap, let liveBroadcastingTimeout = location.liveBroadcastingTimeout { - if entry.message.timestamp + liveBroadcastingTimeout > timestamp { + if liveBroadcastingTimeout == liveLocationIndefinitePeriod || entry.message.timestamp + liveBroadcastingTimeout > timestamp { result.append(entry.message) } } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index fb16b0f37e..58a9ed5d87 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -982,8 +982,9 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { self.inlineStickerLayers = [stickerLayer] } stickerLayer.isVisibleForAnimations = self.visibility != .none + stickerLayer.dynamicColor = file.isCustomTemplateEmoji ? mainColor : nil stickerLayer.frame = inlineMediaFrame - } else if contentAnimatedFilesValue.count == 4 { + } else if contentAnimatedFilesValue.count == 4, let file = contentAnimatedFilesValue.first { var stickerLayers: [InlineStickerItemLayer] = [] if self.inlineStickerLayers.count == contentAnimatedFilesValue.count { stickerLayers = self.inlineStickerLayers @@ -995,14 +996,16 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } self.inlineStickerLayers = stickerLayers } + var frames: [CGRect] = [] let smallSize = CGSize(width: inlineMediaFrame.width / 2.0, height: inlineMediaFrame.width / 2.0) - frames.append(CGRect(origin: inlineMediaFrame.origin, size: smallSize)) - frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: smallSize.width, dy: 0.0), size: smallSize)) - frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: 0.0, dy: smallSize.height), size: smallSize)) - frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: smallSize.width, dy: smallSize.height), size: smallSize)) + frames.append(CGRect(origin: inlineMediaFrame.origin, size: smallSize).insetBy(dx: 2.0, dy: 2.0)) + frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: smallSize.width, dy: 0.0), size: smallSize).insetBy(dx: 2.0, dy: 2.0)) + frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: 0.0, dy: smallSize.height), size: smallSize).insetBy(dx: 2.0, dy: 2.0)) + frames.append(CGRect(origin: inlineMediaFrame.origin.offsetBy(dx: smallSize.width, dy: smallSize.height), size: smallSize).insetBy(dx: 2.0, dy: 2.0)) for i in 0 ..< stickerLayers.count { stickerLayers[i].frame = frames[i] + stickerLayers[i].dynamicColor = file.isCustomTemplateEmoji ? mainColor : nil } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index c1f4322937..b5a1394c36 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -85,7 +85,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { selectedMedia = telegramMap if let liveBroadcastingTimeout = telegramMap.liveBroadcastingTimeout { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if item.message.timestamp != scheduleWhenOnlineTimestamp && item.message.timestamp + liveBroadcastingTimeout > timestamp { + if item.message.timestamp != scheduleWhenOnlineTimestamp && (liveBroadcastingTimeout == liveLocationIndefinitePeriod || item.message.timestamp + liveBroadcastingTimeout > timestamp) { activeLiveBroadcastingTimeout = liveBroadcastingTimeout } } @@ -402,7 +402,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { let timerForegroundColor: UIColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentControlColor : item.presentationData.theme.theme.chat.message.outgoing.accentControlColor let timerTextColor: UIColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor - strongSelf.liveTimerNode?.update(backgroundColor: timerForegroundColor.withAlphaComponent(0.4), foregroundColor: timerForegroundColor, textColor: timerTextColor, beginTimestamp: Double(item.message.timestamp), timeout: Double(activeLiveBroadcastingTimeout), strings: item.presentationData.strings) + strongSelf.liveTimerNode?.update(backgroundColor: timerForegroundColor.withAlphaComponent(0.4), foregroundColor: timerForegroundColor, textColor: timerTextColor, beginTimestamp: Double(item.message.timestamp), timeout: activeLiveBroadcastingTimeout == liveLocationIndefinitePeriod ? -1.0 : Double(activeLiveBroadcastingTimeout), strings: item.presentationData.strings) if strongSelf.liveTextNode == nil { let liveTextNode = ChatMessageLiveLocationTextNode() @@ -421,20 +421,25 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.liveTextNode?.update(color: timerTextColor, timestamp: Double(updateTimestamp), strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) - let timeoutDeadline = item.message.timestamp + activeLiveBroadcastingTimeout - if strongSelf.timeoutTimer?.1 != timeoutDeadline { + if activeLiveBroadcastingTimeout != liveLocationIndefinitePeriod { + let timeoutDeadline = item.message.timestamp + activeLiveBroadcastingTimeout + if strongSelf.timeoutTimer?.1 != timeoutDeadline { + strongSelf.timeoutTimer?.0.invalidate() + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + let timer = SwiftSignalKit.Timer(timeout: Double(max(0, timeoutDeadline - currentTimestamp)), repeat: false, completion: { + if let strongSelf = self { + strongSelf.timeoutTimer?.0.invalidate() + strongSelf.timeoutTimer = nil + item.controllerInteraction.requestMessageUpdate(item.message.id, false) + } + }, queue: Queue.mainQueue()) + strongSelf.timeoutTimer = (timer, timeoutDeadline) + timer.start() + } + } else { strongSelf.timeoutTimer?.0.invalidate() - let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - - let timer = SwiftSignalKit.Timer(timeout: Double(max(0, timeoutDeadline - currentTimestamp)), repeat: false, completion: { - if let strongSelf = self { - strongSelf.timeoutTimer?.0.invalidate() - strongSelf.timeoutTimer = nil - item.controllerInteraction.requestMessageUpdate(item.message.id, false) - } - }, queue: Queue.mainQueue()) - strongSelf.timeoutTimer = (timer, timeoutDeadline) - timer.start() + strongSelf.timeoutTimer = nil } } else { if let liveTimerNode = strongSelf.liveTimerNode { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index f23092d8bf..37d5717a94 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -207,10 +207,14 @@ public final class MediaEditor { } public var resultIsVideo: Bool { + if self.values.entities.contains(where: { $0.entity.isAnimated }) { + return true + } if case let .sticker(file) = self.subject { return file.isAnimatedSticker || file.isVideoSticker + } else { + return self.player != nil || self.audioPlayer != nil || self.additionalPlayer != nil } - return self.player != nil || self.audioPlayer != nil || self.additionalPlayer != nil || self.values.entities.contains(where: { $0.entity.isAnimated }) } public var resultImage: UIImage? { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 25cddca6d5..f01e8633c7 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -6744,54 +6744,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate dismissImpl?() completion() - self.updateEditProgress(0.0, cancel: { [weak self] in - self?.stickerUploadDisposable.set(nil) - }) - self.stickerUploadDisposable.set((self.context.engine.stickers.createStickerSet( - title: title ?? "", - shortName: "", - stickers: [ - ImportSticker( - resource: .standalone(resource: file.resource), - thumbnailResource: file.previewRepresentations.first.flatMap { .standalone(resource: $0.resource) }, - emojis: self.effectiveStickerEmoji(), - dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), - duration: file.duration, - mimeType: file.mimeType, - keywords: "" - ) - ], - thumbnail: nil, - type: .stickers(content: .image), - software: nil - ) |> deliverOnMainQueue).startStandalone(next: { [weak self] status in - guard let self else { - return - } - switch status { - case let .progress(progress, _, _): - self.updateEditProgress(progress, cancel: { [weak self] in - self?.stickerUploadDisposable.set(nil) - }) - case let .complete(info, items): - self.completion(MediaEditorScreen.Result(), { [weak self] finished in - self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in - guard let self else { - return - } - let navigationController = self.navigationController as? NavigationController - self.dismiss() - if let navigationController { - Queue.mainQueue().after(0.2) { - let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) - let controller = self.context.sharedContext.makeStickerPackScreen(context: self.context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [.result(info: info, items: items, installed: true)], isEditing: false, expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: self.sendSticker) - (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) - } - } - }) - }) - } - })) + if let title { + self.uploadSticker(file, action: .createStickerPack(title: title)) + } }, cancel: {}) dismissImpl = { [weak controller] in controller?.dismiss() @@ -6855,38 +6810,38 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let signal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> castError(UploadStickerError.self) - |> mapToSignal { peer -> Signal in + |> mapToSignal { peer -> Signal<(UploadStickerStatus, (StickerPackReference, String)?), UploadStickerError> in guard let peer else { return .complete() } return resourceSignal - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal<(UploadStickerStatus, (StickerPackReference, String)?), UploadStickerError> in switch result { case .failed: return .fail(.generic) case let .progress(progress): - return .single(.progress(progress * 0.5)) + return .single((.progress(progress * 0.5), nil)) case let .complete(resource): if let resource = resource as? CloudDocumentMediaResource { - return .single(.progress(1.0)) |> then(.single(.complete(resource, mimeType))) + return .single((.progress(1.0), nil)) |> then(.single((.complete(resource, mimeType), nil))) } else { return context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: resource, thumbnail: file.previewRepresentations.first?.resource, alt: "", dimensions: dimensions, duration: duration, mimeType: mimeType) - |> mapToSignal { status -> Signal in + |> mapToSignal { status -> Signal<(UploadStickerStatus, (StickerPackReference, String)?), UploadStickerError> in switch status { case let .progress(progress): - return .single(.progress(isVideo ? 0.5 + progress * 0.5 : progress)) + return .single((.progress(isVideo ? 0.5 + progress * 0.5 : progress), nil)) case let .complete(resource, _): let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: file.size ?? 0, dimensions: dimensions, duration: file.duration, isVideo: isVideo) switch action { case .send: - return .single(status) + return .single((status, nil)) case .addToFavorites: return context.engine.stickers.toggleStickerSaved(file: file, saved: true) |> `catch` { _ -> Signal in return .fail(.generic) } |> map { _ in - return status + return (status, nil) } case let .createStickerPack(title): let sticker = ImportSticker( @@ -6902,13 +6857,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return .fail(.generic) } |> mapToSignal { innerStatus in - if case .complete = innerStatus { - return .single(status) + if case let .complete(info, _) = innerStatus { + return .single((status, (.id(id: info.id.id, accessHash: info.accessHash), title))) } else { return .complete() } } - case let .addToStickerPack(pack, _): + case let .addToStickerPack(pack, title): let sticker = ImportSticker( resource: .standalone(resource: resource), emojis: emojis, @@ -6922,10 +6877,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return .fail(.generic) } |> map { _ in - return status + return (status, (pack, title)) } case .upload, .update: - return .single(status) + return .single((status, nil)) } } } @@ -6934,7 +6889,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } self.stickerUploadDisposable.set((signal - |> deliverOnMainQueue).startStandalone(next: { [weak self] status in + |> deliverOnMainQueue).startStandalone(next: { [weak self] (status, packReferenceAndTitle) in guard let self else { return } @@ -6976,15 +6931,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let parentController = navigationController?.viewControllers.last as? ViewController { parentController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_StickerAddedToFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } - case let .addToStickerPack(packReference, title): - let navigationController = self.navigationController as? NavigationController - if let navigationController { + case .addToStickerPack, .createStickerPack: + if let (packReference, packTitle) = packReferenceAndTitle, let navigationController = self.navigationController as? NavigationController { Queue.mainQueue().after(0.2) { let controller = self.context.sharedContext.makeStickerPackScreen(context: self.context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: false, expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: self.sendSticker) (navigationController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) Queue.mainQueue().after(0.1) { - controller.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: presentationData.strings.StickerPack_StickerAdded(title).string, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + controller.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: self.context, file: file, loop: true, title: nil, text: presentationData.strings.StickerPack_StickerAdded(packTitle).string, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index ac932d47ba..1cfaed2e0e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2547,7 +2547,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto contentRequiredValidation = true } else if message.flags.contains(.Incoming), let media = media as? TelegramMediaMap, let liveBroadcastingTimeout = media.liveBroadcastingTimeout { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if message.timestamp + liveBroadcastingTimeout > timestamp { + if liveBroadcastingTimeout == liveLocationIndefinitePeriod || message.timestamp + liveBroadcastingTimeout > timestamp { messageIdsWithLiveLocation.append(message.id) } } else if let telegramFile = media as? TelegramMediaFile {