diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b83fa6a26d..74ff7ee393 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -3765,6 +3765,7 @@ Unused sets are archived when you add more."; "AuthCode.Alert" = "Your login code is %@. Enter it in the Telegram app where you are trying to log in.\n\nDo not give this code to anyone."; "Login.CheckOtherSessionMessages" = "Check your Telegram messages"; "Login.SendCodeViaSms" = "Send the code as an SMS"; +"Login.SendCodeViaCall" = "Get code on my phone"; "Login.CancelPhoneVerification" = "Do you want to stop the phone number verification process?"; "Login.CancelPhoneVerificationStop" = "Stop"; "Login.CancelPhoneVerificationContinue" = "Continue"; @@ -7079,9 +7080,9 @@ Sorry for the inconvenience."; "AuthSessions.View.LocationInfo" = "This location estimate is based on the IP address and may not always be accurate."; -"AuthSessions.View.AcceptSecretChatsTitle" = "Incoming Secret Chats"; -"AuthSessions.View.AcceptSecretChats" = "Accept on This Device"; -"AuthSessions.View.AcceptSecretChatsInfo" = "You can disable the acception of incoming secret chats on this device."; +"AuthSessions.View.AcceptTitle" = "Accept on This Device"; +"AuthSessions.View.AcceptSecretChats" = "New Secret Chats"; +"AuthSessions.View.AcceptIncomingCalls" = "Incoming Calls"; "Conversation.SendMesageAs" = "Send Message As..."; "Conversation.InviteRequestAdminGroup" = "%1$@ is an admin of %2$@, a group you requested to join."; @@ -7101,3 +7102,24 @@ Sorry for the inconvenience."; "AuthSessions.TerminateOtherSessionsText" = "Are you sure you want to terminate all other sessions?"; "Notifications.ResetAllNotificationsText" = "Are you sure you want to reset all notification settings to default?"; + +"MessageCalendar.ClearHistoryForThisDay" = "Clear History For This Day"; +"MessageCalendar.ClearHistoryForTheseDays" = "Clear History For These Days"; +"MessageCalendar.EmptySelectionTooltip" = "Please select one or more days first."; +"Chat.MessageRangeDeleted.ForMe_1" = "Messages for 1 day deleted."; +"Chat.MessageRangeDeleted.ForMe_any" = "Messages for %@ days deleted."; +"Chat.MessageRangeDeleted.ForBothSides_1" = "Messages for 1 day deleted for both sides."; +"Chat.MessageRangeDeleted.ForBothSides_any" = "Messages for %@ days deleted for both sides."; + +"ForcedPasswordSetup.Intro.Title" = "Set a Password"; +"ForcedPasswordSetup.Intro.Text" = "If you want to log into your account frequently, please choose a password."; +"ForcedPasswordSetup.Intro.Action" = "Set a Password"; +"ForcedPasswordSetup.Intro.DoneAction" = "Done"; +"ForcedPasswordSetup.Intro.DismissTitle" = "Warning"; +"ForcedPasswordSetup.Intro.DismissText_1" = "Proceed without a password? If you do not set a password, you will only be able to log into your account via SMS once every **day**."; +"ForcedPasswordSetup.Intro.DismissText_any" = "Proceed without a password? If you do not set a password, you will only be able to log into your account via SMS once every **%@ days**."; +"ForcedPasswordSetup.Intro.DismissActionCancel" = "No, let me set a password"; +"ForcedPasswordSetup.Intro.DismissActionOK" = "Yes, I’m sure"; + +"Login.CodePhonePatternInfoText" = "Please enter the last digits\nof the missed call number."; +"Login.EnterMissingDigits" = "Enter the missing digits"; diff --git a/submodules/AuthorizationUI/Sources/AuthorizationOptionText.swift b/submodules/AuthorizationUI/Sources/AuthorizationOptionText.swift index a3c8f2f70b..fe71055aef 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationOptionText.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationOptionText.swift @@ -45,7 +45,14 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType, } else { switch currentType { case .otherSession: - return (NSAttributedString(string: strings.Login_SendCodeViaSms, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + switch nextType { + case .sms: + return (NSAttributedString(string: strings.Login_SendCodeViaSms, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + case .call, .flashCall, .missedCall: + return (NSAttributedString(string: strings.Login_SendCodeViaCall, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + case .none: + return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) + } default: return (NSAttributedString(string: strings.Login_HaveNotReceivedCodeInternal, font: Font.regular(16.0), textColor: accentColor, paragraphAlignment: .center), true) } diff --git a/submodules/CalendarMessageScreen/BUILD b/submodules/CalendarMessageScreen/BUILD index 88c9da47b7..db198e2a0a 100644 --- a/submodules/CalendarMessageScreen/BUILD +++ b/submodules/CalendarMessageScreen/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/PhotoResources:PhotoResources", "//submodules/DirectMediaImageCache:DirectMediaImageCache", "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/TooltipUI:TooltipUI", ], visibility = [ "//visibility:public", diff --git a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift index 0f88c83891..9680abb33b 100644 --- a/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift +++ b/submodules/CalendarMessageScreen/Sources/CalendarMessageScreen.swift @@ -11,6 +11,7 @@ import ComponentFlow import PhotoResources import DirectMediaImageCache import TelegramStringFormatting +import TooltipUI private final class NullActionClass: NSObject, CAAction { @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { @@ -896,21 +897,20 @@ private final class MonthComponent: CombinedComponent { let dayEnvironment = context.environment[DayEnvironment.self].value let dayItemSize = updatedDays[0].size - let deltaWidth = floor((weekdayWidth - dayItemSize.width) / 2.0) - let deltaHeight = floor((weekdaySize - dayItemSize.width) / 2.0) + let selectionRadius: CGFloat = min(dayItemSize.width, dayItemSize.height) + + let deltaWidth = floor((weekdayWidth - selectionRadius) / 2.0) + let deltaHeight = floor((weekdaySize - selectionRadius) / 2.0) let minX = sideInset + CGFloat(selection.range.lowerBound) * weekdayWidth + deltaWidth let maxX = sideInset + CGFloat(selection.range.upperBound + 1) * weekdayWidth - deltaWidth let minY = baseDayY + CGFloat(lineIndex) * (weekdaySize + weekdaySpacing) + deltaHeight - let maxY = minY + dayItemSize.width - - let leftRadius: CGFloat = dayItemSize.width - let rightRadius: CGFloat = dayItemSize.width + let maxY = minY + selectionRadius let monthSelectionColor = context.component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.1) let selectionRect = CGRect(origin: CGPoint(x: minX, y: minY), size: CGSize(width: maxX - minX, height: maxY - minY)) let selection = selections[lineIndex].update( - component: AnyComponent(ImageComponent(image: dayEnvironment.imageCache.monthSelection(leftRadius: leftRadius, rightRadius: rightRadius, maxRadius: dayItemSize.width, color: monthSelectionColor))), + component: AnyComponent(ImageComponent(image: dayEnvironment.imageCache.monthSelection(leftRadius: selectionRadius, rightRadius: selectionRadius, maxRadius: selectionRadius, color: monthSelectionColor))), availableSize: selectionRect.size, transition: .immediate ) @@ -923,7 +923,7 @@ private final class MonthComponent: CombinedComponent { } let delay = Double(min(delayIndex, 6)) * 0.1 view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.05, delay: delay) - view.layer.animateFrame(from: CGRect(origin: view.frame.origin, size: CGSize(width: leftRadius, height: view.frame.height)), to: view.frame, duration: 0.25, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) + view.layer.animateFrame(from: CGRect(origin: view.frame.origin, size: CGSize(width: selectionRadius, height: view.frame.height)), to: view.frame, duration: 0.25, delay: delay, timingFunction: kCAMediaTimingFunctionSpring) }) .disappear(Transition.Disappear { view, transition, completion in if case .none = transition.animation { @@ -1314,37 +1314,36 @@ public final class CalendarMessageScreen: ViewController { if let selectionState = self.selectionState { let selectionToolbarNode: ToolbarNode - if let currrent = self.selectionToolbarNode { - selectionToolbarNode = currrent + let toolbarText: String + + var selectedCount = 0 + if let dayRange = selectionState.dayRange { + for i in 0 ..< self.months.count { + let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970) - var selectedCount = 0 - if let dayRange = selectionState.dayRange { - for i in 0 ..< self.months.count { - let firstDayTimestamp = Int32(self.months[i].firstDay.timeIntervalSince1970) + for day in 0 ..< self.months[i].numberOfDays { + let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day) - for day in 0 ..< self.months[i].numberOfDays { - let dayTimestamp = firstDayTimestamp + 24 * 60 * 60 * Int32(day) - - if dayRange.contains(dayTimestamp) { - selectedCount += 1 - } + if dayRange.contains(dayTimestamp) { + selectedCount += 1 } } } - - let toolbarText: String - if selectedCount == 0 { - toolbarText = self.presentationData.strings.DialogList_ClearHistoryConfirmation - } else if selectedCount == 1 { - //TODO:localize - toolbarText = "Clear History For This Day" - } else { - //TODO:localize - toolbarText = "Clear History For These Days" - } + } + + if selectedCount == 0 { + toolbarText = self.presentationData.strings.DialogList_ClearHistoryConfirmation + } else if selectedCount == 1 { + toolbarText = self.presentationData.strings.MessageCalendar_ClearHistoryForThisDay + } else { + toolbarText = self.presentationData.strings.MessageCalendar_ClearHistoryForTheseDays + } + + if let currrent = self.selectionToolbarNode { + selectionToolbarNode = currrent transition.updateFrame(node: selectionToolbarNode, frame: tabBarFrame) - selectionToolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: toolbarText, isEnabled: self.selectionState?.dayRange != nil, color: .custom(self.presentationData.theme.list.itemDestructiveColor))), transition: transition) + selectionToolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: toolbarText, isEnabled: true, color: .custom(self.selectionState?.dayRange != nil ? self.presentationData.theme.list.itemDestructiveColor : self.presentationData.theme.list.itemDisabledTextColor))), transition: transition) } else { selectionToolbarNode = ToolbarNode( theme: TabBarControllerTheme( @@ -1359,7 +1358,7 @@ public final class CalendarMessageScreen: ViewController { } ) selectionToolbarNode.frame = tabBarFrame - selectionToolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: self.presentationData.strings.DialogList_ClearHistoryConfirmation, isEnabled: self.selectionState?.dayRange != nil, color: .custom(self.presentationData.theme.list.itemDestructiveColor))), transition: .immediate) + selectionToolbarNode.updateLayout(size: tabBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: Toolbar(leftAction: nil, rightAction: nil, middleAction: ToolbarAction(title: toolbarText, isEnabled: true, color: .custom(self.selectionState?.dayRange != nil ? self.presentationData.theme.list.itemDestructiveColor : self.presentationData.theme.list.itemDisabledTextColor))), transition: .immediate) self.addSubnode(selectionToolbarNode) self.selectionToolbarNode = selectionToolbarNode transition.animatePositionAdditive(node: selectionToolbarNode, offset: CGPoint(x: 0.0, y: tabBarFrame.height)) @@ -1425,6 +1424,16 @@ public final class CalendarMessageScreen: ViewController { } private func selectionToolbarActionSelected() { + if self.selectionState?.dayRange == nil { + if let selectionToolbarNode = self.selectionToolbarNode { + let toolbarFrame = selectionToolbarNode.view.convert(selectionToolbarNode.bounds, to: self.view) + self.controller?.present(TooltipScreen(account: self.context.account, text: self.presentationData.strings.MessageCalendar_EmptySelectionTooltip, style: .default, icon: .none, location: .point(toolbarFrame.insetBy(dx: 0.0, dy: 10.0), .bottom), shouldDismissOnTouch: { point in + return .dismiss(consume: false) + }), in: .current) + } + + return + } guard let selectionState = self.selectionState, let dayRange = selectionState.dayRange else { return } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index da886e862e..1bdd92a0a9 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -24,6 +24,7 @@ import TelegramIntents import TooltipUI import TelegramCallsUI import StickerResources +import PasswordSetupUI private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -1264,6 +1265,53 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) ], actionLayout: .vertical, parseMarkdown: true), in: .window(.root)) })) + + Queue.mainQueue().after(1.0, { + let _ = (self.context.account.postbox.transaction { transaction -> Int32? in + if let value = transaction.getNoticeEntry(key: ApplicationSpecificNotice.forcedPasswordSetupKey())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return nil + } + } + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + + guard let value = value else { + return + } + + let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(strongSelf.context.engine), mode: .intro(.init( + title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_Title, + text: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_Text, + actionText: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_Action, + doneText: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DoneAction + ))) + controller.dismissConfirmation = { [weak controller] f in + guard let strongSelf = self, let controller = controller else { + return true + } + + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissTitle, text: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissText(value), actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissActionCancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ForcedPasswordSetup_Intro_DismissActionOK, action: { [weak controller] in + if let strongSelf = self { + let _ = ApplicationSpecificNotice.setForcedPasswordSetup(postbox: strongSelf.context.account.postbox, reloginDaysTimeout: nil).start() + } + controller?.dismiss() + }) + ], parseMarkdown: true), in: .window(.root)) + + return false + } + strongSelf.push(controller) + + let _ = value + }) + }) } self.chatListDisplayNode.containerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in diff --git a/submodules/Display/Source/DisplayLinkAnimator.swift b/submodules/Display/Source/DisplayLinkAnimator.swift index 2aa9fd0299..39759b6a9d 100644 --- a/submodules/Display/Source/DisplayLinkAnimator.swift +++ b/submodules/Display/Source/DisplayLinkAnimator.swift @@ -101,9 +101,9 @@ public final class ConstantDisplayLinkAnimator { self.displayLink = CADisplayLink(target: DisplayLinkTarget({ [weak self] in self?.tick() }), selector: #selector(DisplayLinkTarget.event)) - if #available(iOS 15.0, *) { + /*if #available(iOS 15.0, *) { self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 120.0, preferred: 120.0) - } + }*/ self.displayLink.isPaused = true self.displayLink.add(to: RunLoop.main, forMode: .common) } diff --git a/submodules/GalleryUI/BUILD b/submodules/GalleryUI/BUILD index d7df405df9..d98f10b4c9 100644 --- a/submodules/GalleryUI/BUILD +++ b/submodules/GalleryUI/BUILD @@ -34,6 +34,10 @@ swift_library( "//submodules/ContextUI:ContextUI", "//submodules/SaveToCameraRoll:SaveToCameraRoll", "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/ImageContentAnalysis:ImageContentAnalysis", + "//submodules/TextSelectionNode:TextSelectionNode", + "//submodules/Speak:Speak", + "//submodules/UndoUI:UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index b3df37dbef..296165a905 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -1122,6 +1122,7 @@ public class GalleryController: ViewController, StandalonePresentableController strongSelf.centralItemRightBarButtonItems.set(node.rightBarButtonItems()) strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) strongSelf.centralItemFooterContentNode.set(node.footerContent()) + strongSelf.galleryNode.pager.pagingEnabledPromise.set(node.isPagingEnabled()) } switch strongSelf.source { @@ -1286,6 +1287,7 @@ public class GalleryController: ViewController, StandalonePresentableController self.centralItemRightBarButtonItems.set(centralItemNode.rightBarButtonItems()) self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) self.centralItemFooterContentNode.set(centralItemNode.footerContent()) + self.galleryNode.pager.pagingEnabledPromise.set(centralItemNode.isPagingEnabled()) if let (media, _) = mediaForMessage(message: message) { if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, media) { @@ -1323,6 +1325,7 @@ public class GalleryController: ViewController, StandalonePresentableController self.centralItemRightBarButtonItems.set(centralItemNode.rightBarButtonItems()) self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) self.centralItemFooterContentNode.set(centralItemNode.footerContent()) + self.galleryNode.pager.pagingEnabledPromise.set(centralItemNode.isPagingEnabled()) if let _ = mediaForMessage(message: message) { centralItemNode.activateAsInitial() diff --git a/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift index 679c685ada..ac3c146d15 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterContentNode.swift @@ -47,7 +47,7 @@ open class GalleryOverlayContentNode: ASDisplayNode { self.visibilityAlpha = alpha } - open func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + open func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, isHidden: Bool, transition: ContainedViewLayoutTransition) { } open func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) { diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index 8ec0a40ffb..c90850f27f 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -101,7 +101,7 @@ public final class GalleryFooterNode: ASDisplayNode { let contentTransition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) if let overlayContentNode = self.currentOverlayContentNode { - overlayContentNode.updateLayout(size: layout.size, metrics: layout.metrics, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: backgroundHeight, transition: transition) + overlayContentNode.updateLayout(size: layout.size, metrics: layout.metrics, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: isHidden ? layout.intrinsicInsets.bottom : backgroundHeight, isHidden: isHidden, transition: transition) transition.updateFrame(node: overlayContentNode, frame: CGRect(origin: CGPoint(), size: layout.size)) if animateOverlayIn { diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index c777929084..bfb4cc7c21 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -58,6 +58,10 @@ open class GalleryItemNode: ASDisplayNode { return .single(nil) } + open func isPagingEnabled() -> Signal { + return .single(true) + } + open func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { return .single((nil, nil)) } diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index cbb6e27361..6c59f7cbfd 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -114,6 +114,10 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } + private var pagingEnabled = true + public var pagingEnabledPromise = Promise(true) + private var pagingEnabledDisposable: Disposable? + public init(pageGap: CGFloat, disableTapNavigation: Bool) { self.pageGap = pageGap self.disableTapNavigation = disableTapNavigation @@ -146,6 +150,17 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest self.addSubnode(self.leftFadeNode) self.addSubnode(self.rightFadeNode) + + self.pagingEnabledDisposable = (self.pagingEnabledPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] pagingEnabled in + if let strongSelf = self { + strongSelf.pagingEnabled = pagingEnabled + } + }) + } + + deinit { + self.pagingEnabledDisposable?.dispose() } public override func didLoad() { @@ -155,7 +170,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest recognizer.delegate = self self.tapRecognizer = recognizer recognizer.tapActionAtPoint = { [weak self] point in - guard let strongSelf = self else { + guard let strongSelf = self, strongSelf.pagingEnabled else { return .fail } @@ -186,7 +201,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest return .keepWithSingleTap } recognizer.highlight = { [weak self] point in - guard let strongSelf = self else { + guard let strongSelf = self, strongSelf.pagingEnabled else { return } let size = strongSelf.bounds diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index a7e6afffe7..ff3ddfa40e 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -13,6 +13,11 @@ import AppBundle import StickerPackPreviewUI import OverlayStatusController import PresentationDataUtils +import ImageContentAnalysis +import TextSelectionNode +import Speak +import ShareController +import UndoUI enum ChatMediaGalleryThumbnail: Equatable { case image(ImageMediaReference) @@ -188,6 +193,10 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private var message: Message? private let imageNode: TransformImageNode + private var recognizedContentNode: RecognizedContentContainer? + + private let recognitionOverlayContentNode: ImageRecognitionOverlayContentNode + private var tilingNode: TilingNode? fileprivate let _ready = Promise() fileprivate let _title = Promise() @@ -203,8 +212,13 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private var fetchDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() private let dataDisposable = MetaDisposable() + private let recognitionDisposable = MetaDisposable() private var status: MediaResourceStatus? + private var textCopiedTooltipController: UndoOverlayController? + + private let pagingEnabledPromise = ValuePromise(true) + init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context @@ -214,6 +228,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.footerContentNode.performAction = performAction self.footerContentNode.openActionOptions = openActionOptions + self.recognitionOverlayContentNode = ImageRecognitionOverlayContentNode(theme: presentationData.theme) + self.statusNodeContainer = HighlightableButtonNode() self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) @@ -237,12 +253,31 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.titleContentView = GalleryTitleView(frame: CGRect()) self._titleView.set(.single(self.titleContentView)) + + self.recognitionOverlayContentNode.action = { [weak self] active in + if let strongSelf = self { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + if let recognizedContentNode = strongSelf.recognizedContentNode { + strongSelf.imageNode.isUserInteractionEnabled = active + transition.updateAlpha(node: recognizedContentNode, alpha: active ? 1.0 : 0.0) + if !active { + recognizedContentNode.dismissSelection() + } + strongSelf.pagingEnabledPromise.set(!active) + } + } + } + } + + override func isPagingEnabled() -> Signal { + return self.pagingEnabledPromise.get() } deinit { //self.fetchDisposable.dispose() self.statusDisposable.dispose() self.dataDisposable.dispose() + self.recognitionDisposable.dispose() } override func ready() -> Signal { @@ -277,6 +312,69 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { switch quality { case .medium, .full: strongSelf.statusNodeContainer.isHidden = true + + Queue.concurrentDefaultQueue().async { + if let message = strongSelf.message, !message.isCopyProtected(), let image = generate(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))?.generateImage() { + strongSelf.recognitionDisposable.set((recognizedContent(postbox: strongSelf.context.account.postbox, image: image, messageId: message.id) + |> deliverOnMainQueue).start(next: { [weak self] results in + if let strongSelf = self { + strongSelf.recognizedContentNode?.removeFromSupernode() + if !results.isEmpty { + let size = strongSelf.imageNode.bounds.size + let recognizedContentNode = RecognizedContentContainer(size: size, image: image, recognitions: results, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, present: { c, a in + strongSelf.galleryController()?.presentInGlobalOverlay(c, with: a) + }, performAction: { [weak self] string, action in + guard let strongSelf = self else { + return + } + switch action { + case .copy: + UIPasteboard.general.string = string + if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with({ $0 }) + let tooltipController = UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }) + strongSelf.textCopiedTooltipController = tooltipController + controller.present(tooltipController, in: .window(.root)) + } + case .share: + if let controller = strongSelf.baseNavigationController()?.topViewController as? ViewController { + let shareController = ShareController(context: strongSelf.context, subject: .text(string), externalShare: true, immediateExternalShare: false, updatedPresentationData: (strongSelf.context.sharedContext.currentPresentationData.with({ $0 }), strongSelf.context.sharedContext.presentationData)) + controller.present(shareController, in: .window(.root)) + } + case .lookup: + let controller = UIReferenceLibraryViewController(term: string) + if let window = strongSelf.baseNavigationController()?.view.window { + controller.popoverPresentationController?.sourceView = window + controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) + window.rootViewController?.present(controller, animated: true) + } + case .speak: + speakText(string) + } + }) + recognizedContentNode.barcodeAction = { [weak self] payload, rect in + guard let strongSelf = self, let message = strongSelf.message else { + return + } + strongSelf.footerContentNode.openActionOptions?(.url(url: payload, concealed: true), message) + } + recognizedContentNode.textAction = { _, _ in +// guard let strongSelf = self else { +// return +// } + } + recognizedContentNode.alpha = 0.0 + recognizedContentNode.frame = CGRect(origin: CGPoint(), size: size) + recognizedContentNode.update(size: strongSelf.imageNode.bounds.size, transition: .immediate) + strongSelf.imageNode.addSubnode(recognizedContentNode) + strongSelf.recognizedContentNode = recognizedContentNode + strongSelf.recognitionOverlayContentNode.transitionIn() + } + } + })) + } + } + case .none, .blurred: strongSelf.statusNodeContainer.isHidden = false } @@ -533,6 +631,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + self.textCopiedTooltipController?.dismiss() + self.fetchDisposable.set(nil) let contentNode = self.tilingNode ?? self.imageNode @@ -629,7 +729,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { - return .single((self.footerContentNode, nil)) + return .single((self.footerContentNode, self.recognitionOverlayContentNode)) } @objc func statusPressed() { @@ -885,3 +985,222 @@ private final class TilingNode: ASDisplayNode { } } } + +extension UIBezierPath { + convenience init(rect: RecognizedContent.Rect, radius r: CGFloat) { + let left = CGFloat.pi + let up = CGFloat.pi * 1.5 + let down = CGFloat.pi * 0.5 + let right = CGFloat.pi * 0.0 + + self.init() + + addArc(withCenter: CGPoint(x: rect.topLeft.x + r, y: rect.topLeft.y + r), radius: r, startAngle: left, endAngle: up, clockwise: true) + addArc(withCenter: CGPoint(x: rect.topRight.x - r, y: rect.topRight.y + r), radius: r, startAngle: up, endAngle: right, clockwise: true) + addArc(withCenter: CGPoint(x: rect.bottomRight.x - r, y: rect.bottomRight.y - r), radius: r, startAngle: right, endAngle: down, clockwise: true) + addArc(withCenter: CGPoint(x: rect.bottomLeft.x + r, y: rect.bottomLeft.y - r), radius: r, startAngle: down, endAngle: left, clockwise: true) + close() + } +} + +private func generateMaskImage(size: CGSize, recognitions: [RecognizedContent]) -> UIImage? { + return generateImage(size, opaque: false, rotatedContext: { size, c in + let bounds = CGRect(origin: CGPoint(), size: size) + c.clear(bounds) + + c.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor) + c.fill(bounds) + + c.setBlendMode(.clear) + for recognition in recognitions { + let mappedRect = recognition.rect.convertTo(size: size, insets: UIEdgeInsets(top: -4.0, left: -2.0, bottom: -4.0, right: -2.0)) + let path = UIBezierPath(rect: mappedRect, radius: 3.5) + c.addPath(path.cgPath) + c.fillPath() + } + }) +} + +private class RecognizedContentContainer: ASDisplayNode { + private let size: CGSize + private let recognitions: [RecognizedContent] + + private let maskNode: ASImageNode + private var selectionNode: RecognizedTextSelectionNode? + + var barcodeAction: ((String, CGRect) -> Void)? + var textAction: ((String, CGRect) -> Void)? + + init(size: CGSize, image: UIImage, recognitions: [RecognizedContent], presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void, performAction: @escaping (String, RecognizedTextSelectionAction) -> Void) { + self.size = size + self.recognitions = recognitions + + self.maskNode = ASImageNode() + self.maskNode.image = generateMaskImage(size: size, recognitions: recognitions) + + super.init() + + let selectionNode = RecognizedTextSelectionNode(size: size, theme: RecognizedTextSelectionTheme(selection: presentationData.theme.chat.message.incoming.textSelectionColor, knob: presentationData.theme.chat.message.incoming.textSelectionKnobColor, knobDiameter: 12.0), strings: presentationData.strings, recognitions: recognitions, updateIsActive: { _ in }, present: present, rootNode: self, performAction: { string, action in + performAction(string, action) + }) + self.selectionNode = selectionNode + + self.addSubnode(self.maskNode) + self.addSubnode(selectionNode.highlightAreaNode) + self.addSubnode(selectionNode) + } + + func dismissSelection() { + let _ = self.selectionNode?.dismissSelection() + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))) + } + + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + let location = gestureRecognizer.location(in: self.view) + + for recognition in self.recognitions { + let mappedRect = recognition.rect.convertTo(size: self.bounds.size) + if mappedRect.boundingFrame.contains(location) { + if case let .qrCode(payload) = recognition.content { + self.barcodeAction?(payload, mappedRect.boundingFrame) + } + break + } + } + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let bounds = CGRect(origin: CGPoint(), size: size) + transition.updateFrame(node: self.maskNode, frame: bounds) + if let selectionNode = self.selectionNode { + transition.updateFrame(node: selectionNode, frame: bounds) + selectionNode.highlightAreaNode.frame = bounds + } + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + for recognition in self.recognitions { + let mappedRect = recognition.rect.convertTo(size: self.bounds.size) + if mappedRect.boundingFrame.insetBy(dx: -20.0, dy: -20.0).contains(point) { + return true + } + } + + if (self.selectionNode?.dismissSelection() ?? false) { + return true + } + + return false + } +} + + +private class ImageRecognitionOverlayContentNode: GalleryOverlayContentNode { + private let backgroundNode: ASImageNode + private let selectedBackgroundNode: ASImageNode + private let iconNode: ASImageNode + private let buttonNode: HighlightTrackingButtonNode + + var action: ((Bool) -> Void)? + private var appeared = false + + init(theme: PresentationTheme) { + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.image = generateFilledCircleImage(diameter: 32.0, color: UIColor(white: 0.0, alpha: 0.6)) + + self.selectedBackgroundNode = ASImageNode() + self.selectedBackgroundNode.displaysAsynchronously = false + self.selectedBackgroundNode.isHidden = true + self.selectedBackgroundNode.image = generateFilledCircleImage(diameter: 32.0, color: theme.list.itemAccentColor) + + self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode.alpha = 0.0 + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/LiveTextIcon"), color: .white) + self.iconNode.contentMode = .center + + super.init() + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.addSubnode(self.buttonNode) + self.buttonNode.addSubnode(self.backgroundNode) + self.buttonNode.addSubnode(self.selectedBackgroundNode) + self.buttonNode.addSubnode(self.iconNode) + } + + @objc private func buttonPressed() { + let newValue = !self.buttonNode.isSelected + self.action?(newValue) + self.buttonNode.isSelected = newValue + self.selectedBackgroundNode.isHidden = !newValue + + if self.interfaceIsHidden && !newValue { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(node: self.buttonNode, alpha: 0.0) + } + } + + func transitionIn() { + guard self.buttonNode.alpha.isZero else { + return + } + self.appeared = true + self.buttonNode.alpha = 1.0 + self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + private var interfaceIsHidden: Bool = false + override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, isHidden: Bool, transition: ContainedViewLayoutTransition) { + self.interfaceIsHidden = isHidden + + let buttonSize = CGSize(width: 32.0, height: 32.0) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: buttonSize) + self.selectedBackgroundNode.frame = CGRect(origin: CGPoint(), size: buttonSize) + self.iconNode.frame = CGRect(origin: CGPoint(), size: buttonSize) + + if self.appeared { + if !self.buttonNode.isSelected && isHidden { + transition.updateAlpha(node: self.buttonNode, alpha: 0.0) + } else { + transition.updateAlpha(node: self.buttonNode, alpha: 1.0) + } + } + + transition.updateFrame(node: self.buttonNode, frame: CGRect(x: size.width - rightInset - buttonSize.width - 12.0, y: size.height - bottomInset - buttonSize.height - 12.0, width: buttonSize.width, height: buttonSize.height)) + } + + override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) { + guard self.appeared && (!self.interfaceIsHidden || self.buttonNode.isSelected) else { + return + } + self.buttonNode.alpha = 1.0 + if let previousContentNode = previousContentNode as? ImageRecognitionOverlayContentNode, previousContentNode.appeared { + + } else { + self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + + override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + let previousAlpha = self.buttonNode.alpha + self.buttonNode.alpha = 0.0 + self.buttonNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2) + completion() + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if self.buttonNode.alpha > 0.0 && self.buttonNode.frame.contains(point) { + return true + } else { + return false + } + } +} diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 1506aa3f98..c2cc1d8bce 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -203,7 +203,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN self.fullscreenNode.addTarget(self, action: #selector(self.toggleFullscreenPressed), forControlEvents: .touchUpInside) } - override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, isHidden: Bool, transition: ContainedViewLayoutTransition) { self.validLayout = (size, metrics, leftInset, rightInset, bottomInset) let isLandscape = size.width > size.height @@ -235,7 +235,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN self.wrapperNode.alpha = self.visibilityAlpha if let validLayout = self.validLayout { - self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, transition: .animated(duration: 0.3, curve: .easeInOut)) + self.updateLayout(size: validLayout.0, metrics: validLayout.1, leftInset: validLayout.2, rightInset: validLayout.3, bottomInset: validLayout.4, isHidden: false, transition: .animated(duration: 0.3, curve: .easeInOut)) } } diff --git a/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift new file mode 100644 index 0000000000..667c268f42 --- /dev/null +++ b/submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift @@ -0,0 +1,540 @@ +import Foundation +import UIKit +import UIKit.UIGestureRecognizerSubclass +import AsyncDisplayKit +import Display +import TelegramPresentationData +import ImageContentAnalysis + +private func findScrollView(view: UIView?) -> UIScrollView? { + if let view = view { + if let view = view as? UIScrollView { + return view + } + return findScrollView(view: view.superview) + } else { + return nil + } +} + +private func cancelScrollViewGestures(view: UIView?) { + if let view = view { + if let gestureRecognizers = view.gestureRecognizers { + for recognizer in gestureRecognizers { + if let recognizer = recognizer as? UIPanGestureRecognizer { + switch recognizer.state { + case .began, .possible: + recognizer.state = .ended + default: + break + } + } + } + } + cancelScrollViewGestures(view: view.superview) + } +} + +private func generateKnobImage(color: UIColor, diameter: CGFloat, inverted: Bool = false) -> UIImage? { + let f: (CGSize, CGContext) -> Void = { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: CGPoint(x: (size.width - 2.0) / 2.0, y: size.width / 2.0), size: CGSize(width: 2.0, height: size.height - size.width / 2.0 - 1.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: (size.width - 2.0) / 2.0, y: size.width + 2.0), size: CGSize(width: 2.0, height: 2.0))) + } + let size = CGSize(width: 12.0, height: 12.0 + 2.0 + 2.0) + if inverted { + return generateImage(size, contextGenerator: f)?.stretchableImage(withLeftCapWidth: Int(size.width / 2.0), topCapHeight: Int(size.height) - (Int(size.width) + 1)) + } else { + return generateImage(size, rotatedContext: f)?.stretchableImage(withLeftCapWidth: Int(size.width / 2.0), topCapHeight: Int(size.width) + 1) + } +} + +private func generateSelectionsImage(size: CGSize, rects: [RecognizedContent.Rect], color: UIColor) -> UIImage? { + return generateImage(size, opaque: false, rotatedContext: { size, c in + let bounds = CGRect(origin: CGPoint(), size: size) + c.clear(bounds) + + c.setFillColor(color.cgColor) + for rect in rects { + let path = UIBezierPath(rect: rect, radius: 2.5) + c.addPath(path.cgPath) + c.fillPath() + } + }) +} + +public final class RecognizedTextSelectionTheme { + public let selection: UIColor + public let knob: UIColor + public let knobDiameter: CGFloat + + public init(selection: UIColor, knob: UIColor, knobDiameter: CGFloat = 12.0) { + self.selection = selection + self.knob = knob + self.knobDiameter = knobDiameter + } +} + +private enum Knob { + case left + case right +} + +private final class RecognizedTextSelectionGetureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + private var longTapTimer: Timer? + private var movingKnob: (Knob, CGPoint, CGPoint)? + private var currentLocation: CGPoint? + + var beginSelection: ((CGPoint) -> Void)? + var knobAtPoint: ((CGPoint) -> (Knob, CGPoint)?)? + var moveKnob: ((Knob, CGPoint) -> Void)? + var finishedMovingKnob: (() -> Void)? + var clearSelection: (() -> Void)? + + override init(target: Any?, action: Selector?) { + super.init(target: nil, action: nil) + + self.delegate = self + } + + override public func reset() { + super.reset() + + self.longTapTimer?.invalidate() + self.longTapTimer = nil + + self.movingKnob = nil + self.currentLocation = nil + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + let currentLocation = touches.first?.location(in: self.view) + self.currentLocation = currentLocation + + if let currentLocation = currentLocation { + if let (knob, knobPosition) = self.knobAtPoint?(currentLocation) { + self.movingKnob = (knob, knobPosition, currentLocation) + cancelScrollViewGestures(view: self.view?.superview) + self.state = .began + } else if self.longTapTimer == nil { + final class TimerTarget: NSObject { + let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + } + + @objc func event() { + self.f() + } + } + let longTapTimer = Timer(timeInterval: 0.3, target: TimerTarget({ [weak self] in + self?.longTapEvent() + }), selector: #selector(TimerTarget.event), userInfo: nil, repeats: false) + self.longTapTimer = longTapTimer + RunLoop.main.add(longTapTimer, forMode: .common) + } + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + let currentLocation = touches.first?.location(in: self.view) + self.currentLocation = currentLocation + + if let (knob, initialKnobPosition, initialGesturePosition) = self.movingKnob, let currentLocation = currentLocation { + self.moveKnob?(knob, CGPoint(x: initialKnobPosition.x + currentLocation.x - initialGesturePosition.x, y: initialKnobPosition.y + currentLocation.y - initialGesturePosition.y)) + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + if let longTapTimer = self.longTapTimer { + self.longTapTimer = nil + longTapTimer.invalidate() + self.clearSelection?() + } else { + if let _ = self.currentLocation, let _ = self.movingKnob { + self.finishedMovingKnob?() + } + } + self.state = .ended + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.state = .cancelled + } + + private func longTapEvent() { + if let currentLocation = self.currentLocation { + self.beginSelection?(currentLocation) + self.state = .ended + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + return true + } + + @available(iOS 9.0, *) + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool { + return true + } +} + +public final class RecognizedTextSelectionNodeView: UIView { + var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return self.hitTestImpl?(point, event) + } +} + +public enum RecognizedTextSelectionAction { + case copy + case share + case lookup + case speak +} + +public final class RecognizedTextSelectionNode: ASDisplayNode { + private let size: CGSize + private let theme: RecognizedTextSelectionTheme + private let strings: PresentationStrings + private let recognitions: [(string: String, rect: RecognizedContent.Rect)] + private let updateIsActive: (Bool) -> Void + private let present: (ViewController, Any?) -> Void + private weak var rootNode: ASDisplayNode? + private let performAction: (String, RecognizedTextSelectionAction) -> Void + private var highlightOverlay: ASImageNode? + private let leftKnob: ASImageNode + private let rightKnob: ASImageNode + + private var selectedIndices: Set? + private var currentRects: [RecognizedContent.Rect]? + private var currentTopLeft: CGPoint? + private var currentBottomRight: CGPoint? + + public let highlightAreaNode: ASDisplayNode + + private var recognizer: RecognizedTextSelectionGetureRecognizer? + + public init(size: CGSize, theme: RecognizedTextSelectionTheme, strings: PresentationStrings, recognitions: [RecognizedContent], updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (String, RecognizedTextSelectionAction) -> Void) { + self.size = size + self.theme = theme + self.strings = strings + + let sortedRecognitions = recognitions.sorted(by: { lhs, rhs in + if abs(lhs.rect.leftMidPoint.y - rhs.rect.rightMidPoint.y) < min(lhs.rect.leftHeight, rhs.rect.leftHeight) / 2.0 { + return lhs.rect.leftMidPoint.x < rhs.rect.leftMidPoint.x + } else { + return lhs.rect.leftMidPoint.y > rhs.rect.leftMidPoint.y + } + }) + var textRecognitions: [(String, RecognizedContent.Rect)] = [] + for recognition in sortedRecognitions { + if case let .text(string, _) = recognition.content { + textRecognitions.append((string, recognition.rect)) +// for word in words { +// textRecognitions.append((String(string[word.0]), word.1)) +// } + } + } + self.recognitions = textRecognitions + + self.updateIsActive = updateIsActive + self.present = present + self.rootNode = rootNode + self.performAction = performAction + self.leftKnob = ASImageNode() + self.leftKnob.isUserInteractionEnabled = false + self.leftKnob.image = generateKnobImage(color: theme.knob, diameter: theme.knobDiameter) + self.leftKnob.displaysAsynchronously = false + self.leftKnob.displayWithoutProcessing = true + self.leftKnob.alpha = 0.0 + self.rightKnob = ASImageNode() + self.rightKnob.isUserInteractionEnabled = false + self.rightKnob.image = generateKnobImage(color: theme.knob, diameter: theme.knobDiameter, inverted: true) + self.rightKnob.displaysAsynchronously = false + self.rightKnob.displayWithoutProcessing = true + self.rightKnob.alpha = 0.0 + + self.highlightAreaNode = ASDisplayNode() + + super.init() + + self.setViewBlock({ + return RecognizedTextSelectionNodeView() + }) + + self.addSubnode(self.leftKnob) + self.addSubnode(self.rightKnob) + } + + override public func didLoad() { + super.didLoad() + + (self.view as? RecognizedTextSelectionNodeView)?.hitTestImpl = { [weak self] point, event in + return self?.hitTest(point, with: event) + } + + let recognizer = RecognizedTextSelectionGetureRecognizer(target: nil, action: nil) + recognizer.knobAtPoint = { [weak self] point in + return self?.knobAtPoint(point) + } + recognizer.moveKnob = { [weak self] knob, point in + guard let strongSelf = self, let _ = strongSelf.selectedIndices, let currentTopLeft = strongSelf.currentTopLeft, let currentBottomRight = strongSelf.currentBottomRight else { + return + } + + let topLeftPoint: CGPoint + let bottomRightPoint: CGPoint + switch knob { + case .left: + topLeftPoint = point + bottomRightPoint = currentBottomRight + case .right: + topLeftPoint = currentTopLeft + bottomRightPoint = point + } + + let selectionRect = CGRect(x: min(topLeftPoint.x, bottomRightPoint.x), y: min(topLeftPoint.y, bottomRightPoint.y), width: max(bottomRightPoint.x, topLeftPoint.x) - min(bottomRightPoint.x, topLeftPoint.x), height: max(bottomRightPoint.y, topLeftPoint.y) - min(bottomRightPoint.y, topLeftPoint.y)) + + var i = 0 + var selectedIndices: Set? + for recognition in strongSelf.recognitions { + let rect = recognition.rect.convertTo(size: strongSelf.size, insets: UIEdgeInsets(top: -4.0, left: -2.0, bottom: -4.0, right: -2.0)) + if selectionRect.intersects(rect.boundingFrame) { + if selectedIndices == nil { + selectedIndices = Set() + } + selectedIndices?.insert(i) + } + i += 1 + } + + strongSelf.selectedIndices = selectedIndices + strongSelf.updateSelection(range: selectedIndices, animateIn: false) + } + recognizer.finishedMovingKnob = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.displayMenu() + } + recognizer.beginSelection = { [weak self] point in + guard let strongSelf = self else { + return + } + + let _ = strongSelf.dismissSelection() + + var i = 0 + var selectedIndices: Set? + var topLeft: CGPoint? + var bottomRight: CGPoint? + for recognition in strongSelf.recognitions { + let rect = recognition.rect.convertTo(size: strongSelf.size, insets: UIEdgeInsets(top: -4.0, left: -2.0, bottom: -4.0, right: -2.0)) + if rect.boundingFrame.contains(point) { + topLeft = rect.topLeft + bottomRight = rect.bottomRight + selectedIndices = Set([i]) + break + } + i += 1 + } + strongSelf.selectedIndices = selectedIndices + strongSelf.currentTopLeft = topLeft + strongSelf.currentBottomRight = bottomRight + strongSelf.updateSelection(range: selectedIndices, animateIn: true) + + strongSelf.displayMenu() + strongSelf.updateIsActive(true) + } + recognizer.clearSelection = { [weak self] in + let _ = self?.dismissSelection() + self?.updateIsActive(false) + } + self.recognizer = recognizer + self.view.addGestureRecognizer(recognizer) + } + + public func updateLayout() { + if let selectedIndices = self.selectedIndices { + self.updateSelection(range: selectedIndices, animateIn: false) + } + } + + private func updateSelection(range: Set?, animateIn: Bool) { + var rects: [RecognizedContent.Rect]? = nil + var startEdge: (position: CGPoint, height: CGFloat)? + var endEdge: (position: CGPoint, height: CGFloat)? + + if let range = range { + var i = 0 + rects = [] + for recognition in self.recognitions { + let rect = recognition.rect.convertTo(size: self.size) + if range.contains(i) { + if startEdge == nil { + startEdge = (rect.leftMidPoint, rect.leftHeight) + } + rects?.append(rect) + } + i += 1 + } + + if let rect = rects?.last { + endEdge = (rect.rightMidPoint, rect.rightHeight) + } + } + + self.currentRects = rects + + if let rects = rects, let startEdge = startEdge, let endEdge = endEdge, !rects.isEmpty { + let highlightOverlay: ASImageNode + if let current = self.highlightOverlay { + highlightOverlay = current + } else { + highlightOverlay = ASImageNode() + self.highlightOverlay = highlightOverlay + self.highlightAreaNode.addSubnode(highlightOverlay) + } + highlightOverlay.frame = self.bounds + highlightOverlay.image = generateSelectionsImage(size: self.size, rects: rects, color: self.theme.selection.withAlphaComponent(1.0)) + highlightOverlay.alpha = self.theme.selection.alpha + + if let image = self.leftKnob.image { + self.leftKnob.frame = CGRect(origin: CGPoint(x: floor(startEdge.position.x - image.size.width / 2.0), y: startEdge.position.y - floorToScreenPixels(startEdge.height / 2.0) - self.theme.knobDiameter), size: CGSize(width: image.size.width, height: self.theme.knobDiameter + startEdge.height + 2.0)) + self.rightKnob.frame = CGRect(origin: CGPoint(x: floor(endEdge.position.x + 1.0 - image.size.width / 2.0), y: endEdge.position.y - floorToScreenPixels(endEdge.height / 2.0)), size: CGSize(width: image.size.width, height: self.theme.knobDiameter + endEdge.height + 2.0)) + } + if self.leftKnob.alpha.isZero { + highlightOverlay.layer.animateAlpha(from: 0.0, to: highlightOverlay.alpha, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + self.leftKnob.alpha = 1.0 + self.leftKnob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.14, delay: 0.19) + self.rightKnob.alpha = 1.0 + self.rightKnob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.14, delay: 0.19) + self.leftKnob.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.2, delay: 0.25, initialVelocity: 0.0, damping: 80.0) + self.rightKnob.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.2, delay: 0.25, initialVelocity: 0.0, damping: 80.0) + + if animateIn { + var result = CGRect() + for rect in rects { + if result.isEmpty { + result = rect.boundingFrame + } else { + result = result.union(rect.boundingFrame) + } + } + highlightOverlay.layer.animateScale(from: 2.0, to: 1.0, duration: 0.26) + let fromResult = CGRect(origin: CGPoint(x: result.minX - result.width / 2.0, y: result.minY - result.height / 2.0), size: CGSize(width: result.width * 2.0, height: result.height * 2.0)) + highlightOverlay.layer.animatePosition(from: CGPoint(x: (-fromResult.midX + highlightOverlay.bounds.midX) / 1.0, y: (-fromResult.midY + highlightOverlay.bounds.midY) / 1.0), to: CGPoint(), duration: 0.26, additive: true) + } + } + } else if let highlightOverlay = self.highlightOverlay { + self.highlightOverlay = nil + highlightOverlay.layer.animateAlpha(from: highlightOverlay.alpha, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak highlightOverlay] _ in + highlightOverlay?.removeFromSupernode() + }) + self.leftKnob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + self.leftKnob.alpha = 0.0 + self.leftKnob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18) + self.rightKnob.alpha = 0.0 + self.rightKnob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18) + } + } + + private func knobAtPoint(_ point: CGPoint) -> (Knob, CGPoint)? { + if !self.leftKnob.alpha.isZero, self.leftKnob.frame.insetBy(dx: -4.0, dy: -8.0).contains(point) { + return (.left, self.leftKnob.frame.offsetBy(dx: 0.0, dy: self.leftKnob.frame.width / 2.0).center) + } + if !self.rightKnob.alpha.isZero, self.rightKnob.frame.insetBy(dx: -4.0, dy: -8.0).contains(point) { + return (.right, self.rightKnob.frame.offsetBy(dx: 0.0, dy: -self.rightKnob.frame.width / 2.0).center) + } + if !self.leftKnob.alpha.isZero, self.leftKnob.frame.insetBy(dx: -14.0, dy: -14.0).contains(point) { + return (.left, self.leftKnob.frame.offsetBy(dx: 0.0, dy: self.leftKnob.frame.width / 2.0).center) + } + if !self.rightKnob.alpha.isZero, self.rightKnob.frame.insetBy(dx: -14.0, dy: -14.0).contains(point) { + return (.right, self.rightKnob.frame.offsetBy(dx: 0.0, dy: -self.rightKnob.frame.width / 2.0).center) + } + return nil + } + + public func dismissSelection() -> Bool { + if let _ = self.selectedIndices { + self.selectedIndices = nil + self.updateSelection(range: nil, animateIn: false) + return true + } else { + return false + } + } + + private func displayMenu() { + guard let currentRects = self.currentRects, !currentRects.isEmpty, let selectedIndices = self.selectedIndices else { + return + } + + var completeRect = currentRects[0].boundingFrame + for i in 0 ..< currentRects.count { + completeRect = completeRect.union(currentRects[i].boundingFrame) + } + completeRect = completeRect.insetBy(dx: 0.0, dy: -12.0) + + var selectedText = "" + for i in 0 ..< self.recognitions.count { + if selectedIndices.contains(i) { + let (string, _) = self.recognitions[i] + if !selectedText.isEmpty { + selectedText += "\n" + } + selectedText.append(contentsOf: string.trimmingCharacters(in: .whitespacesAndNewlines)) + } + } + + var actions: [ContextMenuAction] = [] + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in + self?.performAction(selectedText, .copy) + let _ = self?.dismissSelection() + })) + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuLookUp, accessibilityLabel: self.strings.Conversation_ContextMenuLookUp), action: { [weak self] in + self?.performAction(selectedText, .lookup) + let _ = self?.dismissSelection() + })) + if isSpeakSelectionEnabled() { + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuSpeak, accessibilityLabel: self.strings.Conversation_ContextMenuSpeak), action: { [weak self] in + self?.performAction(selectedText, .speak) + let _ = self?.dismissSelection() + })) + } + actions.append(ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in + self?.performAction(selectedText, .share) + let _ = self?.dismissSelection() + })) + + self.present(ContextMenuController(actions: actions, catchTapsOutside: false, hasHapticFeedback: false), ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in + guard let strongSelf = self, let rootNode = strongSelf.rootNode else { + return nil + } + return (strongSelf, completeRect, rootNode, rootNode.bounds) + }, bounce: false)) + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.knobAtPoint(point) != nil { + return self.view + } + if self.bounds.contains(point) { + return self.view + } + return nil + } +} diff --git a/submodules/ImageContentAnalysis/BUILD b/submodules/ImageContentAnalysis/BUILD new file mode 100644 index 0000000000..07d864765b --- /dev/null +++ b/submodules/ImageContentAnalysis/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ImageContentAnalysis", + module_name = "ImageContentAnalysis", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + #"-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramUIPreferences:TelegramUIPreferences", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift b/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift new file mode 100644 index 0000000000..28961f2782 --- /dev/null +++ b/submodules/ImageContentAnalysis/Sources/ImageContentAnalysis.swift @@ -0,0 +1,343 @@ +import Foundation +import UIKit +import Vision +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramUIPreferences + +private final class CachedImageRecognizedContent: Codable { + public let results: [RecognizedContent] + + public init(results: [RecognizedContent]) { + self.results = results + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.results = try container.decode([RecognizedContent].self, forKey: "results") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.results, forKey: "results") + } +} + +private func cachedImageRecognizedContent(postbox: Postbox, messageId: MessageId) -> Signal { + return postbox.transaction { transaction -> CachedImageRecognizedContent? in + let key = ValueBoxKey(length: 8) + key.setInt32(0, value: messageId.namespace) + key.setInt32(4, value: messageId.id) + if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.cachedImageRecognizedContent, key: key))?.get(CachedImageRecognizedContent.self) { + return entry + } else { + return nil + } + } +} + +private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 50, highWaterItemCount: 100) + +private func updateCachedImageRecognizedContent(postbox: Postbox, messageId: MessageId, content: CachedImageRecognizedContent?) -> Signal { + return postbox.transaction { transaction -> Void in + let key = ValueBoxKey(length: 8) + key.setInt32(0, value: messageId.namespace) + key.setInt32(4, value: messageId.id) + let id = ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.cachedImageRecognizedContent, key: key) + if let content = content, let entry = CodableEntry(content) { + transaction.putItemCacheEntry(id: id, entry: entry, collectionSpec: collectionSpec) + } else { + transaction.removeItemCacheEntry(id: id) + } + } +} + +extension CGPoint { + func distanceTo(_ a: CGPoint) -> CGFloat { + let xDist = a.x - x + let yDist = a.y - y + return CGFloat(sqrt((xDist * xDist) + (yDist * yDist))) + } + + func midPoint(_ other: CGPoint) -> CGPoint { + return CGPoint(x: (self.x + other.x) / 2.0, y: (self.y + other.y) / 2.0) + } +} + +public struct RecognizedContent: Codable { + public enum Content { + case text(text: String, words: [(Range, Rect)]) + case qrCode(payload: String) + } + + public struct Rect: Codable { + struct Point: Codable { + let x: Double + let y: Double + + init(cgPoint: CGPoint) { + self.x = cgPoint.x + self.y = cgPoint.y + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.x = try container.decode(Double.self, forKey: "x") + self.y = try container.decode(Double.self, forKey: "y") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.x, forKey: "x") + try container.encode(self.y, forKey: "y") + } + + var cgPoint: CGPoint { + return CGPoint(x: self.x, y: self.y) + } + } + + public let topLeft: CGPoint + public let topRight: CGPoint + public let bottomLeft: CGPoint + public let bottomRight: CGPoint + + public var boundingFrame: CGRect { + let top: CGFloat = min(topLeft.y, topRight.y) + let left: CGFloat = min(topLeft.x, bottomLeft.x) + let right: CGFloat = max(topRight.x, bottomRight.x) + let bottom: CGFloat = max(bottomLeft.y, bottomRight.y) + return CGRect(x: left, y: top, width: abs(right - left), height: abs(bottom - top)) + } + + public var leftMidPoint: CGPoint { + return self.topLeft.midPoint(self.bottomLeft) + } + + public var leftHeight: CGFloat { + return self.topLeft.distanceTo(self.bottomLeft) + } + + public var rightMidPoint: CGPoint { + return self.topRight.midPoint(self.bottomRight) + } + + public var rightHeight: CGFloat { + return self.topRight.distanceTo(self.bottomRight) + } + + public func convertTo(size: CGSize, insets: UIEdgeInsets = UIEdgeInsets()) -> Rect { + return Rect( + topLeft: CGPoint(x: self.topLeft.x * size.width + insets.left, y: size.height - self.topLeft.y * size.height + insets.top), + topRight: CGPoint(x: self.topRight.x * size.width - insets.right, y: size.height - self.topRight.y * size.height + insets.top), + bottomLeft: CGPoint(x: self.bottomLeft.x * size.width + insets.left, y: size.height - self.bottomLeft.y * size.height - insets.bottom), + bottomRight: CGPoint(x: self.bottomRight.x * size.width - insets.right, y: size.height - self.bottomRight.y * size.height - insets.bottom) + ) + } + + public init() { + self.topLeft = CGPoint() + self.topRight = CGPoint() + self.bottomLeft = CGPoint() + self.bottomRight = CGPoint() + } + + public init(topLeft: CGPoint, topRight: CGPoint, bottomLeft: CGPoint, bottomRight: CGPoint) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomLeft = bottomLeft + self.bottomRight = bottomRight + } + + @available(iOS 11.0, *) + public init(observation: VNRectangleObservation) { + self.topLeft = observation.topLeft + self.topRight = observation.topRight + self.bottomLeft = observation.bottomLeft + self.bottomRight = observation.bottomRight + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.topLeft = try container.decode(Point.self, forKey: "topLeft").cgPoint + self.topRight = try container.decode(Point.self, forKey: "topRight").cgPoint + self.bottomLeft = try container.decode(Point.self, forKey: "bottomLeft").cgPoint + self.bottomRight = try container.decode(Point.self, forKey: "bottomRight").cgPoint + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(Point(cgPoint: self.topLeft), forKey: "topLeft") + try container.encode(Point(cgPoint: self.topRight), forKey: "topRight") + try container.encode(Point(cgPoint: self.bottomLeft), forKey: "bottomLeft") + try container.encode(Point(cgPoint: self.bottomRight), forKey: "bottomRight") + } + } + + public let rect: Rect + public let content: Content + + @available(iOS 11.0, *) + init?(observation: VNObservation) { + if let barcode = observation as? VNBarcodeObservation, case .qr = barcode.symbology, let payload = barcode.payloadStringValue { + self.content = .qrCode(payload: payload) + self.rect = Rect(observation: barcode) + } else if #available(iOS 13.0, *), let text = observation as? VNRecognizedTextObservation, let candidate = text.topCandidates(1).first, candidate.confidence >= 0.5 { + let string = candidate.string + var words: [(Range, Rect)] = [] + string.enumerateSubstrings(in: string.startIndex ..< string.endIndex, options: .byWords) { _, substringRange, _, _ in + if let rectangle = try? candidate.boundingBox(for: substringRange) { + words.append((substringRange, Rect(observation: rectangle))) + } + } + self.content = .text(text: string, words: words) + self.rect = Rect(observation: text) + } else { + return nil + } + } + + struct WordRangeAndRect: Codable { + let start: Int32 + let end: Int32 + let rect: Rect + + init(text: String, range: Range, rect: Rect) { + self.start = Int32(text.distance(from: text.startIndex, to: range.lowerBound)) + self.end = Int32(text.distance(from: text.startIndex, to: range.upperBound)) + self.rect = rect + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.start = try container.decode(Int32.self, forKey: "start") + self.end = try container.decode(Int32.self, forKey: "end") + self.rect = try container.decode(Rect.self, forKey: "rect") + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.start, forKey: "start") + try container.encode(self.end, forKey: "end") + try container.encode(self.rect, forKey: "rect") + } + + func toRangeWithRect(text: String) -> (Range, Rect) { + return (text.index(text.startIndex, offsetBy: Int(self.start)) ..< text.index(text.startIndex, offsetBy: Int(self.end)), self.rect) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + let type = try container.decode(Int32.self, forKey: "t") + if type == 0 { + let text = try container.decode(String.self, forKey: "text") + let rangesWithRects = try container.decode([WordRangeAndRect].self, forKey: "words") + let words = rangesWithRects.map { $0.toRangeWithRect(text: text) } + self.content = .text(text: text, words: words) + self.rect = try container.decode(Rect.self, forKey: "rect") + } else if type == 1 { + let payload = try container.decode(String.self, forKey: "payload") + self.content = .qrCode(payload: payload) + self.rect = try container.decode(Rect.self, forKey: "rect") + } else { + assertionFailure() + self.content = .text(text: "", words: []) + self.rect = Rect() + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + switch self.content { + case let .text(text, words): + try container.encode(Int32(0), forKey: "t") + try container.encode(text, forKey: "text") + + let rangesWithRects: [WordRangeAndRect] = words.map { WordRangeAndRect(text: text, range: $0.0, rect: $0.1) } + try container.encode(rangesWithRects, forKey: "words") + try container.encode(rect, forKey: "rect") + case let .qrCode(payload): + try container.encode(Int32(1), forKey: "t") + try container.encode(payload, forKey: "payload") + try container.encode(rect, forKey: "rect") + } + } +} + +private func recognizeContent(in image: UIImage) -> Signal<[RecognizedContent], NoError> { + if #available(iOS 11.0, *) { + guard let cgImage = image.cgImage else { + return .complete() + } + return Signal { subscriber in + var requests: [VNRequest] = [] + + let barcodeResult = Atomic<[RecognizedContent]?>(value: nil) + let textResult = Atomic<[RecognizedContent]?>(value: nil) + + let completion = { + let barcode = barcodeResult.with { $0 } + let text = textResult.with { $0 } + + if let barcode = barcode, let text = text { + subscriber.putNext(barcode + text) + subscriber.putCompletion() + } + } + + let barcodeRequest = VNDetectBarcodesRequest { request, error in + let mappedResults = request.results?.compactMap { RecognizedContent(observation: $0) } ?? [] + let _ = barcodeResult.swap(mappedResults) + completion() + } + requests.append(barcodeRequest) + + if #available(iOS 13.0, *) { + let textRequest = VNRecognizeTextRequest { request, error in + let mappedResults = request.results?.compactMap { RecognizedContent(observation: $0) } ?? [] + let _ = textResult.swap(mappedResults) + completion() + } + textRequest.usesLanguageCorrection = true + requests.append(textRequest) + } else { + let _ = textResult.swap([]) + } + + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + try? handler.perform(requests) + + return ActionDisposable { + + } + } + } else { + return .single([]) + } +} + +public func recognizedContent(postbox: Postbox, image: UIImage, messageId: MessageId) -> Signal<[RecognizedContent], NoError> { + return cachedImageRecognizedContent(postbox: postbox, messageId: messageId) + |> mapToSignal { cachedContent -> Signal<[RecognizedContent], NoError> in + if let cachedContent = cachedContent { + return .single(cachedContent.results) + } else { + return recognizeContent(in: image) + |> beforeNext { results in + let _ = updateCachedImageRecognizedContent(postbox: postbox, messageId: messageId, content: CachedImageRecognizedContent(results: results)).start() + } + } + } +} diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 9c0a806142..26c380e000 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -162,7 +162,8 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { private let unreadNode: ASImageNode private let titleNode: TextNode private let statusNode: TextNode - private let installationActionImageNode: ASImageNode + private let installTextNode: TextNode + private let installationActionBackgroundNode: ASImageNode private let installationActionNode: HighlightableButtonNode private let selectionIconNode: ASImageNode @@ -234,12 +235,17 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { self.unreadNode.displaysAsynchronously = false self.unreadNode.displayWithoutProcessing = true - self.installationActionImageNode = ASImageNode() - self.installationActionImageNode.displaysAsynchronously = false - self.installationActionImageNode.displayWithoutProcessing = true - self.installationActionImageNode.isLayerBacked = true + self.installationActionBackgroundNode = ASImageNode() + self.installationActionBackgroundNode.displaysAsynchronously = false + self.installationActionBackgroundNode.displayWithoutProcessing = true + self.installationActionBackgroundNode.isLayerBacked = true self.installationActionNode = HighlightableButtonNode() + self.installTextNode = TextNode() + self.installTextNode.isUserInteractionEnabled = false + self.installTextNode.contentMode = .left + self.installTextNode.contentsScale = UIScreen.main.scale + self.selectionIconNode = ASImageNode() self.selectionIconNode.displaysAsynchronously = false self.selectionIconNode.displayWithoutProcessing = true @@ -262,7 +268,8 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { self.containerNode.addSubnode(self.titleNode) self.containerNode.addSubnode(self.statusNode) self.containerNode.addSubnode(self.unreadNode) - self.containerNode.addSubnode(self.installationActionImageNode) + self.containerNode.addSubnode(self.installationActionBackgroundNode) + self.containerNode.addSubnode(self.installTextNode) self.containerNode.addSubnode(self.installationActionNode) self.containerNode.addSubnode(self.selectionIconNode) self.addSubnode(self.activateArea) @@ -271,11 +278,11 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { self.installationActionNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { - strongSelf.installationActionImageNode.layer.removeAnimation(forKey: "opacity") - strongSelf.installationActionImageNode.alpha = 0.4 + strongSelf.installationActionBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.installationActionBackgroundNode.alpha = 0.4 } else { - strongSelf.installationActionImageNode.alpha = 1.0 - strongSelf.installationActionImageNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.installationActionBackgroundNode.alpha = 1.0 + strongSelf.installationActionBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } @@ -334,6 +341,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let makeImageLayout = self.imageNode.asyncLayout() let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + let makeInstallLayout = TextNode.asyncLayout(self.installTextNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode) @@ -365,17 +373,19 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { var rightInset: CGFloat = params.rightInset - var installationActionImage: UIImage? + var installationBackgroundImage: UIImage? + var installationText: String? var checkImage: UIImage? switch item.control { case .none: break case let .installation(installed): - rightInset += 50.0 if installed { - installationActionImage = PresentationResourcesItemList.secondaryCheckIconImage(item.presentationData.theme) + installationBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddedPackButtonImage(item.presentationData.theme) + installationText = item.presentationData.strings.Stickers_Installed } else { - installationActionImage = PresentationResourcesItemList.plusIconImage(item.presentationData.theme) + installationBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddPackButtonImage(item.presentationData.theme) + installationText = item.presentationData.strings.Stickers_Install } case .selection: rightInset += 16.0 @@ -428,9 +438,22 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { reorderInset = sizeAndApply.0 } } + + var installed = false + if case .installation(true) = item.control { + installed = true + } - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 10.0 - reorderInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - reorderInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: installationText ?? "", font: Font.semibold(13.0), textColor: installed ? item.presentationData.theme.list.itemCheckColors.fillColor : item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let installWidth: CGFloat + if installLayout.size.width > 0.0 { + installWidth = installLayout.size.width + 32.0 + } else { + installWidth = 0.0 + } + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - 10.0 - reorderInset - installWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - reorderInset - installWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + statusLayout.size.height) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -626,28 +649,32 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { let _ = titleApply() let _ = statusApply() - - let installationActionFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 50.0, y: 0.0), size: CGSize(width: 50.0, height: layout.contentSize.height)) - strongSelf.installationActionNode.frame = installationActionFrame - + let _ = installApply() + switch item.control { case .none: strongSelf.installationActionNode.isHidden = true - strongSelf.installationActionImageNode.isHidden = true + strongSelf.installationActionBackgroundNode.isHidden = true strongSelf.selectionIconNode.isHidden = true case let .installation(installed): - strongSelf.installationActionImageNode.isHidden = false + strongSelf.installationActionBackgroundNode.isHidden = false strongSelf.installationActionNode.isHidden = false strongSelf.selectionIconNode.isHidden = true strongSelf.installationActionNode.isUserInteractionEnabled = !installed - if let image = installationActionImage { - let imageSize = image.size - strongSelf.installationActionImageNode.image = image - strongSelf.installationActionImageNode.frame = CGRect(origin: CGPoint(x: installationActionFrame.minX + floor((installationActionFrame.size.width - imageSize.width) / 2.0), y: installationActionFrame.minY + floor((installationActionFrame.size.height - imageSize.height) / 2.0)), size: imageSize) + + if let backgroundImage = installationBackgroundImage { + strongSelf.installationActionBackgroundNode.image = backgroundImage } + + let installationActionFrame = CGRect(origin: CGPoint(x: params.width - rightInset - installWidth - 16.0, y: 0.0), size: CGSize(width: 50.0, height: layout.contentSize.height)) + strongSelf.installationActionNode.frame = installationActionFrame + + let buttonFrame = CGRect(origin: CGPoint(x: params.width - rightInset - installWidth - 16.0, y: installationActionFrame.minY + floor((installationActionFrame.size.height - 28.0) / 2.0)), size: CGSize(width: installWidth, height: 28.0)) + strongSelf.installationActionBackgroundNode.frame = buttonFrame + strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size) case .selection: strongSelf.installationActionNode.isHidden = true - strongSelf.installationActionImageNode.isHidden = true + strongSelf.installationActionBackgroundNode.isHidden = true strongSelf.selectionIconNode.isHidden = false if let image = checkImage { strongSelf.selectionIconNode.image = image @@ -655,7 +682,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } case .check: strongSelf.installationActionNode.isHidden = true - strongSelf.installationActionImageNode.isHidden = true + strongSelf.installationActionBackgroundNode.isHidden = true strongSelf.selectionIconNode.isHidden = true } diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index ffd7790f00..02efd889cc 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -35,14 +35,14 @@ public enum TwoFactorDataInputMode { case authorized } - case password - case passwordRecoveryEmail(emailPattern: String, mode: PasswordRecoveryEmailMode) - case passwordRecovery(Recovery) - case emailAddress(password: String, hint: String) - case updateEmailAddress(password: String) - case emailConfirmation(passwordAndHint: (String, String)?, emailPattern: String, codeLength: Int?) - case passwordHint(recovery: Recovery?, password: String) - case rememberPassword + case password(doneText: String) + case passwordRecoveryEmail(emailPattern: String, mode: PasswordRecoveryEmailMode, doneText: String) + case passwordRecovery(recovery: Recovery, doneText: String) + case emailAddress(password: String, hint: String, doneText: String) + case updateEmailAddress(password: String, doneText: String) + case emailConfirmation(passwordAndHint: (String, String)?, emailPattern: String, codeLength: Int?, doneText: String) + case passwordHint(recovery: Recovery?, password: String, doneText: String) + case rememberPassword(doneText: String) } public final class TwoFactorDataInputScreen: ViewController { @@ -110,7 +110,7 @@ public final class TwoFactorDataInputScreen: ViewController { return } switch strongSelf.mode { - case .password: + case let .password(doneText): let values = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText if values.count != 2 { return @@ -136,9 +136,9 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordHint(recovery: nil, password: values[0]), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) + controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordHint(recovery: nil, password: values[0], doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) navigationController.setViewControllers(controllers, animated: true) - case let .passwordRecoveryEmail(_, mode): + case let .passwordRecoveryEmail(_, mode, doneText): guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else { return } @@ -180,14 +180,14 @@ public final class TwoFactorDataInputScreen: ViewController { mappedMode = .notAuthorized(syncContacts: syncContacts) } - let setupController = TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordRecovery(TwoFactorDataInputMode.Recovery(code: text, mode: mappedMode)), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation) + let setupController = TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordRecovery(recovery: TwoFactorDataInputMode.Recovery(code: text, mode: mappedMode), doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation) guard let navigationController = strongSelf.navigationController as? NavigationController else { return } navigationController.replaceController(strongSelf, with: setupController, animated: true) })) - case let .passwordRecovery(recovery): + case let .passwordRecovery(recovery, doneText): let values = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText if values.count != 2 { return @@ -204,8 +204,8 @@ public final class TwoFactorDataInputScreen: ViewController { guard let navigationController = strongSelf.navigationController as? NavigationController else { return } - navigationController.replaceController(strongSelf, with: TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordHint(recovery: recovery, password: values[0]), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation), animated: true) - case let .emailAddress(password, hint): + navigationController.replaceController(strongSelf, with: TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .passwordHint(recovery: recovery, password: values[0], doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation), animated: true) + case let .emailAddress(password, hint, doneText): guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else { return } @@ -237,7 +237,7 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailConfirmation(passwordAndHint: (password, hint), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init)), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) + controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailConfirmation(passwordAndHint: (password, hint), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init), doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) navigationController.setViewControllers(controllers, animated: true) } else { guard let navigationController = strongSelf.navigationController as? NavigationController else { @@ -252,7 +252,7 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done)) + controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done(doneText: doneText))) navigationController.setViewControllers(controllers, animated: true) } } @@ -273,7 +273,7 @@ public final class TwoFactorDataInputScreen: ViewController { } strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) - case let .updateEmailAddress(password): + case let .updateEmailAddress(password, doneText): guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else { return } @@ -307,7 +307,7 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailConfirmation(passwordAndHint: (password, ""), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init)), stateUpdated: strongSelf.stateUpdated)) + controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailConfirmation(passwordAndHint: (password, ""), emailPattern: text, codeLength: pendingEmail.codeLength.flatMap(Int.init), doneText: doneText), stateUpdated: strongSelf.stateUpdated)) navigationController.setViewControllers(controllers, animated: true) } else { guard let navigationController = strongSelf.navigationController as? NavigationController else { @@ -322,7 +322,7 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done)) + controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done(doneText: doneText))) navigationController.setViewControllers(controllers, animated: true) } } @@ -346,7 +346,7 @@ public final class TwoFactorDataInputScreen: ViewController { case .unauthorized: break } - case .emailConfirmation: + case let .emailConfirmation(_, _, _, doneText): guard let text = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !text.isEmpty else { return } @@ -397,13 +397,13 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done)) + controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done(doneText: doneText))) navigationController.setViewControllers(controllers, animated: true) }) case .unauthorized: break } - case let .passwordHint(recovery, password): + case let .passwordHint(recovery, password, doneText): guard let value = (strongSelf.displayNode as! TwoFactorDataInputScreenNode).inputText.first, !value.isEmpty else { return } @@ -411,7 +411,7 @@ public final class TwoFactorDataInputScreen: ViewController { if let recovery = recovery { strongSelf.performRecovery(recovery: recovery, password: password, hint: value) } else { - strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: value), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) + strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: value, doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) } case .rememberPassword: guard case let .authorized(engine) = strongSelf.engine else { @@ -476,7 +476,7 @@ public final class TwoFactorDataInputScreen: ViewController { return } switch strongSelf.mode { - case let .emailAddress(password, hint): + case let .emailAddress(password, hint, doneText): strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationText, actions: [ TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_Email_SkipConfirmationSkip, action: { guard let strongSelf = self else { @@ -509,7 +509,7 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done)) + controllers.append(TwoFactorAuthSplashScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .done(doneText: doneText))) navigationController.setViewControllers(controllers, animated: true) } }, error: { [weak statusController] error in @@ -532,13 +532,13 @@ public final class TwoFactorDataInputScreen: ViewController { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) ]), in: .window(.root)) - case let .passwordHint(recovery, password): + case let .passwordHint(recovery, password, doneText): if let recovery = recovery { strongSelf.performRecovery(recovery: recovery, password: password, hint: "") } else { - strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: ""), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) + strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: "", doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) } - case let .passwordRecovery(recovery): + case let .passwordRecovery(recovery, _): strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertTitle, text: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertText, actions: [ TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.TwoFactorSetup_PasswordRecovery_SkipAlertAction, action: { guard let strongSelf = self else { @@ -548,7 +548,7 @@ public final class TwoFactorDataInputScreen: ViewController { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) ]), in: .window(.root)) - case .rememberPassword: + case let .rememberPassword(doneText): guard case let .authorized(engine) = strongSelf.engine else { return } @@ -575,7 +575,7 @@ public final class TwoFactorDataInputScreen: ViewController { disposable.set((engine.auth.requestTwoStepVerificationPasswordRecoveryCode() |> deliverOnMainQueue).start(next: { emailPattern in var stateUpdated: ((SetupTwoStepVerificationStateUpdate) -> Void)? - let controller = TwoFactorDataInputScreen(sharedContext: sharedContext, engine: .authorized(engine), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .authorized), stateUpdated: { state in + let controller = TwoFactorDataInputScreen(sharedContext: sharedContext, engine: .authorized(engine), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .authorized, doneText: doneText), stateUpdated: { state in stateUpdated?(state) }) stateUpdated = { [weak controller] state in @@ -706,7 +706,7 @@ public final class TwoFactorDataInputScreen: ViewController { return } switch strongSelf.mode { - case let .emailConfirmation(passwordAndHint, _, _): + case let .emailConfirmation(passwordAndHint, _, _, doneText): if let (password, hint) = passwordAndHint { guard let navigationController = strongSelf.navigationController as? NavigationController else { return @@ -720,7 +720,7 @@ public final class TwoFactorDataInputScreen: ViewController { } return true } - controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: hint), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) + controllers.append(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .emailAddress(password: password, hint: hint, doneText: doneText), stateUpdated: strongSelf.stateUpdated, presentation: strongSelf.navigationPresentation)) navigationController.setViewControllers(controllers, animated: true) } else { } @@ -1371,7 +1371,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS toggleTextHidden?(node) }), ] - case let .passwordRecoveryEmail(emailPattern, _): + case let .passwordRecoveryEmail(emailPattern, _, _): title = presentationData.strings.TwoFactorSetup_EmailVerification_Title let formattedString = presentationData.strings.TwoFactorSetup_EmailVerification_Text(emailPattern) @@ -1398,7 +1398,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS toggleTextHidden?(node) }), ] - case let .emailConfirmation(_, emailPattern, _): + case let .emailConfirmation(_, emailPattern, _, _): title = presentationData.strings.TwoFactorSetup_EmailVerification_Title let formattedString = presentationData.strings.TwoFactorSetup_EmailVerification_Text(emailPattern) @@ -1654,7 +1654,7 @@ private final class TwoFactorDataInputScreenNode: ViewControllerTracingNode, UIS strongSelf.buttonNode.isHidden = !hasText strongSelf.skipActionTitleNode.isHidden = hasText strongSelf.skipActionButtonNode.isHidden = hasText - case let .emailConfirmation(_, _, codeLength): + case let .emailConfirmation(_, _, codeLength, _): let text = strongSelf.inputNodes[0].text let hasText = !text.isEmpty strongSelf.buttonNode.isHidden = !hasText diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift index 968092fea7..a3b5cc982f 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift @@ -13,8 +13,27 @@ import PresentationDataUtils import TelegramCore public enum TwoFactorAuthSplashMode { - case intro - case done + public struct Intro { + public var title: String + public var text: String + public var actionText: String + public var doneText: String + + public init( + title: String, + text: String, + actionText: String, + doneText: String + ) { + self.title = title + self.text = text + self.actionText = actionText + self.doneText = doneText + } + } + + case intro(Intro) + case done(doneText: String) case recoveryDone(recoveredAccountData: RecoveredAccountData?, syncContacts: Bool, isPasswordSet: Bool) case remember } @@ -25,6 +44,8 @@ public final class TwoFactorAuthSplashScreen: ViewController { private var presentationData: PresentationData private var mode: TwoFactorAuthSplashMode + public var dismissConfirmation: ((@escaping () -> Void) -> Bool)? + public init(sharedContext: SharedAccountContext, engine: SomeTelegramEngine, mode: TwoFactorAuthSplashMode, presentation: ViewControllerNavigationPresentation = .modalInLargeLayout) { self.sharedContext = sharedContext self.engine = engine @@ -55,6 +76,14 @@ public final class TwoFactorAuthSplashScreen: ViewController { } else { self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: ASDisplayNode()) } + + self.attemptNavigation = { [weak self] f in + guard let strongSelf = self, let dismissConfirmation = strongSelf.dismissConfirmation else { + return true + } + + return dismissConfirmation(f) + } } required init(coder aDecoder: NSCoder) { @@ -70,8 +99,8 @@ public final class TwoFactorAuthSplashScreen: ViewController { return } switch strongSelf.mode { - case .intro: - strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .password, stateUpdated: { _ in + case let .intro(intro): + strongSelf.push(TwoFactorDataInputScreen(sharedContext: strongSelf.sharedContext, engine: strongSelf.engine, mode: .password(doneText: intro.doneText), stateUpdated: { _ in }, presentation: strongSelf.navigationPresentation)) case .done, .remember: guard let navigationController = strongSelf.navigationController as? NavigationController else { @@ -136,18 +165,18 @@ private final class TwoFactorAuthSplashScreenNode: ViewControllerTracingNode { let textColor = self.presentationData.theme.list.itemPrimaryTextColor switch mode { - case .intro: - title = self.presentationData.strings.TwoFactorSetup_Intro_Title - texts = [NSAttributedString(string: self.presentationData.strings.TwoFactorSetup_Intro_Text, font: textFont, textColor: textColor)] - buttonText = self.presentationData.strings.TwoFactorSetup_Intro_Action + case let .intro(intro): + title = intro.title + texts = [NSAttributedString(string: intro.text, font: textFont, textColor: textColor)] + buttonText = intro.actionText self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "TwoFactorSetupIntro"), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.animationSize = CGSize(width: 124.0, height: 124.0) self.animationNode.visibility = true - case .done: + case let .done(doneText): title = self.presentationData.strings.TwoFactorSetup_Done_Title texts = [NSAttributedString(string: self.presentationData.strings.TwoFactorSetup_Done_Text, font: textFont, textColor: textColor)] - buttonText = self.presentationData.strings.TwoFactorSetup_Done_Action + buttonText = doneText self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "TwoFactorSetupDone"), width: 248, height: 248, mode: .direct(cachePathPrefix: nil)) self.animationSize = CGSize(width: 124.0, height: 124.0) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 0e814c733f..bfa6b090ea 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -732,7 +732,13 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting break case let .notSet(pendingEmail): if pendingEmail == nil { - let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .intro) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = TwoFactorAuthSplashScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .intro(.init( + title: presentationData.strings.TwoFactorSetup_Intro_Title, + text: presentationData.strings.TwoFactorSetup_Intro_Text, + actionText: presentationData.strings.TwoFactorSetup_Intro_Action, + doneText: presentationData.strings.TwoFactorSetup_Done_Action + ))) pushControllerImpl?(controller, true) return diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index 4fd84c0b52..340cfaed54 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -703,6 +703,8 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont }, openSession: { session in let controller = RecentSessionScreen(context: context, subject: .session(session), updateAcceptSecretChats: { value in updateSessionDisposable.set(activeSessionsContext.updateSessionAcceptsSecretChats(session, accepts: value).start()) + }, updateAcceptIncomingCalls: { value in + updateSessionDisposable.set(activeSessionsContext.updateSessionAcceptsIncomingCalls(session, accepts: value).start()) }, remove: { completion in removeSessionImpl(session.hash, { completion() @@ -710,7 +712,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont }) presentControllerImpl?(controller, nil) }, openWebSession: { session, peer in - let controller = RecentSessionScreen(context: context, subject: .website(session, peer), updateAcceptSecretChats: { _ in }, remove: { completion in + let controller = RecentSessionScreen(context: context, subject: .website(session, peer), updateAcceptSecretChats: { _ in }, updateAcceptIncomingCalls: { _ in }, remove: { completion in removeWebSessionImpl(session.hash) completion() }) @@ -759,7 +761,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont pushControllerImpl?(AuthTransferScanScreen(context: context, activeSessionsContext: activeSessionsContext)) }) }, openOtherAppsUrl: { - context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://getdesktop.telegram.org", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://telegram.org/apps", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }, setupAuthorizationTTL: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationData: presentationData) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift index f85e6c9182..9139af6d9b 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/RecentSessionScreen.swift @@ -56,6 +56,7 @@ final class RecentSessionScreen: ViewController { private let subject: RecentSessionScreen.Subject private let remove: (@escaping () -> Void) -> Void private let updateAcceptSecretChats: (Bool) -> Void + private let updateAcceptIncomingCalls: (Bool) -> Void private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -70,12 +71,13 @@ final class RecentSessionScreen: ViewController { } } - init(context: AccountContext, subject: RecentSessionScreen.Subject, updateAcceptSecretChats: @escaping (Bool) -> Void, remove: @escaping (@escaping () -> Void) -> Void) { + init(context: AccountContext, subject: RecentSessionScreen.Subject, updateAcceptSecretChats: @escaping (Bool) -> Void, updateAcceptIncomingCalls: @escaping (Bool) -> Void, remove: @escaping (@escaping () -> Void) -> Void) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.subject = subject self.remove = remove self.updateAcceptSecretChats = updateAcceptSecretChats + self.updateAcceptIncomingCalls = updateAcceptIncomingCalls super.init(navigationBarPresentationData: nil) @@ -119,6 +121,9 @@ final class RecentSessionScreen: ViewController { self.controllerNode.updateAcceptSecretChats = { [weak self] value in self?.updateAcceptSecretChats(value) } + self.controllerNode.updateAcceptIncomingCalls = { [weak self] value in + self?.updateAcceptIncomingCalls(value) + } } override public func loadView() { @@ -178,11 +183,13 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe private let locationValueNode: ImmediateTextNode private let locationInfoNode: ImmediateTextNode - private let secretChatsBackgroundNode: ASDisplayNode - private let secretChatsHeaderNode: ImmediateTextNode + private let acceptBackgroundNode: ASDisplayNode + private let acceptHeaderNode: ImmediateTextNode private let secretChatsTitleNode: ImmediateTextNode private let secretChatsSwitchNode: SwitchNode - private let secretChatsInfoNode: ImmediateTextNode + private let incomingCallsTitleNode: ImmediateTextNode + private let incomingCallsSwitchNode: SwitchNode + private let acceptSeparatorNode: ASDisplayNode private let cancelButton: HighlightableButtonNode private let terminateButton: SolidRoundedButtonNode @@ -193,6 +200,7 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe var remove: (() -> Void)? var dismiss: (() -> Void)? var updateAcceptSecretChats: ((Bool) -> Void)? + var updateAcceptIncomingCalls: ((Bool) -> Void)? init(context: AccountContext, presentationData: PresentationData, controller: RecentSessionScreen, subject: RecentSessionScreen.Subject) { self.context = context @@ -249,10 +257,11 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe self.locationValueNode = ImmediateTextNode() self.locationInfoNode = ImmediateTextNode() - self.secretChatsHeaderNode = ImmediateTextNode() + self.acceptHeaderNode = ImmediateTextNode() self.secretChatsTitleNode = ImmediateTextNode() self.secretChatsSwitchNode = SwitchNode() - self.secretChatsInfoNode = ImmediateTextNode() + self.incomingCallsTitleNode = ImmediateTextNode() + self.incomingCallsSwitchNode = SwitchNode() self.cancelButton = HighlightableButtonNode() self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) @@ -330,6 +339,7 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe } self.secretChatsSwitchNode.isOn = session.flags.contains(.acceptsSecretChats) + self.incomingCallsSwitchNode.isOn = session.flags.contains(.acceptsIncomingCalls) if !session.flags.contains(.passwordPending) && ![2040, 2496].contains(session.apiId) { hasSecretChats = true @@ -391,15 +401,17 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe self.locationInfoNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_LocationInfo, font: Font.regular(13.0), textColor: secondaryTextColor) self.locationInfoNode.maximumNumberOfLines = 3 - self.secretChatsBackgroundNode = ASDisplayNode() - self.secretChatsBackgroundNode.clipsToBounds = true - self.secretChatsBackgroundNode.cornerRadius = 11 - self.secretChatsBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor + self.acceptBackgroundNode = ASDisplayNode() + self.acceptBackgroundNode.clipsToBounds = true + self.acceptBackgroundNode.cornerRadius = 11 + self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor - self.secretChatsHeaderNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChatsTitle.uppercased(), font: Font.regular(17.0), textColor: textColor) + self.acceptHeaderNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptTitle.uppercased(), font: Font.regular(17.0), textColor: textColor) self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChats, font: Font.regular(17.0), textColor: textColor) - self.secretChatsInfoNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChatsInfo, font: Font.regular(17.0), textColor: secondaryTextColor) - self.secretChatsInfoNode.maximumNumberOfLines = 3 + self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptIncomingCalls, font: Font.regular(17.0), textColor: textColor) + + self.acceptSeparatorNode = ASDisplayNode() + self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor super.init() @@ -442,18 +454,27 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe self.animationNode.flatMap { self.contentContainerNode.addSubnode($0) } self.avatarNode.flatMap { self.contentContainerNode.addSubnode($0) } + self.contentContainerNode.addSubnode(self.acceptBackgroundNode) + self.contentContainerNode.addSubnode(self.acceptHeaderNode) if hasSecretChats { - self.contentContainerNode.addSubnode(self.secretChatsBackgroundNode) - self.contentContainerNode.addSubnode(self.secretChatsHeaderNode) self.contentContainerNode.addSubnode(self.secretChatsTitleNode) self.contentContainerNode.addSubnode(self.secretChatsSwitchNode) - self.contentContainerNode.addSubnode(self.secretChatsInfoNode) self.secretChatsSwitchNode.valueUpdated = { [weak self] value in if let strongSelf = self { strongSelf.updateAcceptSecretChats?(value) } } + + self.contentContainerNode.addSubnode(self.acceptSeparatorNode) + } + self.contentContainerNode.addSubnode(self.incomingCallsTitleNode) + self.contentContainerNode.addSubnode(self.incomingCallsSwitchNode) + + self.incomingCallsSwitchNode.valueUpdated = { [weak self] value in + if let strongSelf = self { + strongSelf.updateAcceptIncomingCalls?(value) + } } self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) @@ -542,6 +563,8 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe } let previousTheme = self.presentationData.theme self.presentationData = presentationData + + self.contentBackgroundNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.regular(30.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) @@ -556,6 +579,7 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe self.fieldBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor self.firstSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor self.secondSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor + self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor self.deviceTitleNode.attributedText = NSAttributedString(string: self.deviceTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.locationTitleNode.attributedText = NSAttributedString(string: self.locationTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) @@ -566,10 +590,10 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe self.ipValueNode.attributedText = NSAttributedString(string: self.ipValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) self.locationInfoNode.attributedText = NSAttributedString(string: self.locationInfoNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.secretChatsHeaderNode.attributedText = NSAttributedString(string: self.secretChatsHeaderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) + self.acceptHeaderNode.attributedText = NSAttributedString(string: self.acceptHeaderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.secretChatsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - self.secretChatsInfoNode.attributedText = NSAttributedString(string: self.secretChatsInfoNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) - self.secretChatsBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor + self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.incomingCallsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) + self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) @@ -749,33 +773,48 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe var contentHeight = locationInfoTextFrame.maxY + bottomInset + 64.0 - if let _ = self.secretChatsBackgroundNode.supernode { - let secretFrame = CGRect(x: inset, y: locationInfoTextFrame.maxY + 59.0, width: width - inset * 2.0, height: fieldItemHeight) - transition.updateFrame(node: self.secretChatsBackgroundNode, frame: secretFrame) - - let secretChatsHeaderTextSize = self.secretChatsHeaderNode.updateLayout(CGSize(width: secretFrame.width - inset * 2.0 - locationTitleTextSize.width - 10.0, height: fieldItemHeight)) - let secretChatsHeaderTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY - secretChatsHeaderTextSize.height - 6.0), size: secretChatsHeaderTextSize) - transition.updateFrame(node: self.secretChatsHeaderNode, frame: secretChatsHeaderTextFrame) - + var secretFrame = CGRect(x: inset, y: locationInfoTextFrame.maxY + 59.0, width: width - inset * 2.0, height: fieldItemHeight) + if let _ = self.secretChatsTitleNode.supernode { + secretFrame.size.height += fieldItemHeight + } + transition.updateFrame(node: self.acceptBackgroundNode, frame: secretFrame) + + let secretChatsHeaderTextSize = self.acceptHeaderNode.updateLayout(CGSize(width: secretFrame.width - inset * 2.0 - locationTitleTextSize.width - 10.0, height: fieldItemHeight)) + let secretChatsHeaderTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY - secretChatsHeaderTextSize.height - 6.0), size: secretChatsHeaderTextSize) + transition.updateFrame(node: self.acceptHeaderNode, frame: secretChatsHeaderTextFrame) + + if let _ = self.secretChatsTitleNode.supernode { let secretChatsTitleTextSize = self.secretChatsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight)) let secretChatsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - secretChatsTitleTextSize.height) / 2.0)), size: secretChatsTitleTextSize) transition.updateFrame(node: self.secretChatsTitleNode, frame: secretChatsTitleTextFrame) - - let secretChatsInfoTextSize = self.secretChatsInfoNode.updateLayout(CGSize(width: secretFrame.width - inset * 2.0, height: fieldItemHeight)) - let secretChatsInfoTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.maxY + 6.0), size: secretChatsInfoTextSize) - transition.updateFrame(node: self.secretChatsInfoNode, frame: secretChatsInfoTextFrame) - + if let switchView = self.secretChatsSwitchNode.view as? UISwitch { if self.secretChatsSwitchNode.bounds.size.width.isZero { switchView.sizeToFit() } let switchSize = switchView.bounds.size - self.secretChatsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize) + self.secretChatsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize) } - - contentHeight += secretChatsInfoTextFrame.maxY - locationInfoTextFrame.maxY } + + let incomingCallsTitleTextSize = self.incomingCallsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight)) + let incomingCallsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - incomingCallsTitleTextSize.height) / 2.0)), size: incomingCallsTitleTextSize) + transition.updateFrame(node: self.incomingCallsTitleNode, frame: incomingCallsTitleTextFrame) + + transition.updateFrame(node: self.acceptSeparatorNode, frame: CGRect(x: secretFrame.minX + inset, y: secretFrame.minY + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) + + if let switchView = self.incomingCallsSwitchNode.view as? UISwitch { + if self.incomingCallsSwitchNode.bounds.size.width.isZero { + switchView.sizeToFit() + } + let switchSize = switchView.bounds.size + + self.incomingCallsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize) + } + + contentHeight += secretFrame.maxY - locationInfoTextFrame.maxY + contentHeight += 40.0 let isCurrent: Bool if case let .session(session) = self.subject, session.isCurrent { @@ -803,7 +842,7 @@ private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDe transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) let doneButtonHeight = self.terminateButton.updateLayout(width: width - inset * 2.0, transition: transition) - transition.updateFrame(node: self.terminateButton, frame: CGRect(x: inset, y: contentHeight - doneButtonHeight - insets.bottom - 6.0, width: width, height: doneButtonHeight)) + transition.updateFrame(node: self.terminateButton, frame: CGRect(x: inset, y: contentHeight - doneButtonHeight - 40.0 - insets.bottom - 6.0, width: width, height: doneButtonHeight)) transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index aad0182437..4ab4a52ad1 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -509,7 +509,7 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext, } var stateUpdated: ((SetupTwoStepVerificationStateUpdate) -> Void)? - let controller = TwoFactorDataInputScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .authorized), stateUpdated: { state in + let controller = TwoFactorDataInputScreen(sharedContext: context.sharedContext, engine: .authorized(context.engine), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .authorized, doneText: presentationData.strings.TwoFactorSetup_Done_Action), stateUpdated: { state in stateUpdated?(state) }) stateUpdated = { [weak controller] state in diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 9ba35f0361..6624cbd1c0 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -105,11 +105,11 @@ public func sendAuthorizationCode(accountManager: AccountManager `catch` { error -> Signal<(SendCodeResult, UnauthorizedAccount), MTRpcError> in if error.errorDescription == "SESSION_PASSWORD_NEEDED" { - return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false) + return updatedAccount.network.request(Api.functions.account.getPassword(), automaticFloodWait: false) |> mapToSignal { result -> Signal<(SendCodeResult, UnauthorizedAccount), MTRpcError> in switch result { case let .password(_, _, _, _, hint, _, _, _, _, _): - return .single((.password(hint: hint), account)) + return .single((.password(hint: hint), updatedAccount)) } } } else { @@ -252,7 +252,7 @@ public enum AuthorizeWithCodeResult { case loggedIn } -public func authorizeWithCode(accountManager: AccountManager, account: UnauthorizedAccount, code: String, termsOfService: UnauthorizedAccountTermsOfService?) -> Signal { +public func authorizeWithCode(accountManager: AccountManager, account: UnauthorizedAccount, code: String, termsOfService: UnauthorizedAccountTermsOfService?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { @@ -302,11 +302,14 @@ public func authorizeWithCode(accountManager: AccountManager AuthorizeWithCodeResult in switchToAuthorizedAccount(transaction: transaction, account: account) return .loggedIn @@ -542,7 +545,7 @@ public enum SignUpError { case invalidLastName } -public func signUpWithName(accountManager: AccountManager, account: UnauthorizedAccount, firstName: String, lastName: String, avatarData: Data?, avatarVideo: Signal?, videoStartTimestamp: Double?) -> Signal { +public func signUpWithName(accountManager: AccountManager, account: UnauthorizedAccount, firstName: String, lastName: String, avatarData: Data?, avatarVideo: Signal?, videoStartTimestamp: Double?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState, case let .signUp(number, codeHash, _, _, _, syncContacts) = state.contents { return account.network.request(Api.functions.auth.signUp(phoneNumber: number, phoneCodeHash: codeHash, firstName: firstName, lastName: lastName)) @@ -561,7 +564,7 @@ public func signUpWithName(accountManager: AccountManager mapToSignal { result -> Signal in switch result { - case let .authorization(_, _, _, user): + case let .authorization(_, otherwiseReloginDays, _, user): let user = TelegramUser(user: user) let appliedState = account.postbox.transaction { transaction -> Void in let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil) @@ -570,8 +573,11 @@ public func signUpWithName(accountManager: AccountManager castError(SignUpError.self) + } + |> castError(SignUpError.self) let switchedAccounts = accountManager.transaction { transaction -> Void in switchToAuthorizedAccount(transaction: transaction, account: account) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 7ce185e254..c8b868d72c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -99,12 +99,13 @@ public struct AdminLogEventsFlags: OptionSet { public static let deleteMessages = AdminLogEventsFlags(rawValue: 1 << 13) public static let calls = AdminLogEventsFlags(rawValue: 1 << 14) public static let invites = AdminLogEventsFlags(rawValue: 1 << 15) - + public static let sendMessages = AdminLogEventsFlags(rawValue: 1 << 16) + public static var all: AdminLogEventsFlags { - return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites] + return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .sendMessages, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites] } public static var flags: AdminLogEventsFlags { - return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites] + return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .sendMessages, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites] } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/ActiveSessionsContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/ActiveSessionsContext.swift index 8ca22e2575..854ab30f52 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/ActiveSessionsContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/ActiveSessionsContext.swift @@ -144,6 +144,29 @@ private final class ActiveSessionsContextImpl { } } + func updateSessionAcceptsIncomingCalls(_ session: RecentAccountSession, accepts: Bool) -> Signal { + let updatedSession = session.withUpdatedAcceptsIncomingCalls(accepts) + + var mergedSessions = self._state.sessions + for i in 0 ..< mergedSessions.count { + if mergedSessions[i].hash == updatedSession.hash { + mergedSessions.remove(at: i) + mergedSessions.insert(updatedSession, at: i) + break + } + } + self._state = ActiveSessionsContextState(isLoadingMore: self._state.isLoadingMore, sessions: mergedSessions, ttlDays: self._state.ttlDays) + + return updateAccountSessionAcceptsIncomingCalls(account: self.account, hash: session.hash, accepts: accepts) + |> deliverOnMainQueue + |> mapToSignal { [weak self] _ -> Signal in + if let strongSelf = self { + strongSelf._state = ActiveSessionsContextState(isLoadingMore: strongSelf._state.isLoadingMore, sessions: mergedSessions, ttlDays: strongSelf._state.ttlDays) + } + return .complete() + } + } + func updateAuthorizationTTL(days: Int32) -> Signal { self._state = ActiveSessionsContextState(isLoadingMore: self._state.isLoadingMore, sessions: self._state.sessions, ttlDays: days) @@ -233,6 +256,20 @@ public final class ActiveSessionsContext { } } + public func updateSessionAcceptsIncomingCalls(_ session: RecentAccountSession, accepts: Bool) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.updateSessionAcceptsIncomingCalls(session, accepts: accepts).start(error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable + } + } + public func updateAuthorizationTTL(days: Int32) -> Signal { let days = max(1, min(365, days)) return Signal { subscriber in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSession.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSession.swift index 3dcd26533c..ecc0e9c3b9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSession.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSession.swift @@ -15,6 +15,7 @@ public struct AccountSessionFlags: OptionSet { public static let isOfficial = AccountSessionFlags(rawValue: (1 << 1)) public static let passwordPending = AccountSessionFlags(rawValue: (1 << 2)) public static let acceptsSecretChats = AccountSessionFlags(rawValue: (1 << 3)) + public static let acceptsIncomingCalls = AccountSessionFlags(rawValue: (1 << 4)) } public struct RecentAccountSession: Equatable { @@ -88,6 +89,16 @@ public struct RecentAccountSession: Equatable { } return RecentAccountSession(hash: self.hash, deviceModel: self.deviceModel, platform: self.platform, systemVersion: self.systemVersion, apiId: self.apiId, appName: self.appName, appVersion: self.appVersion, creationDate: self.creationDate, activityDate: self.activityDate, ip: self.ip, country: self.country, region: self.region, flags: flags) } + + func withUpdatedAcceptsIncomingCalls(_ accepts: Bool) -> RecentAccountSession { + var flags = self.flags + if accepts { + flags.insert(.acceptsIncomingCalls) + } else { + flags.remove(.acceptsIncomingCalls) + } + return RecentAccountSession(hash: self.hash, deviceModel: self.deviceModel, platform: self.platform, systemVersion: self.systemVersion, apiId: self.apiId, appName: self.appName, appVersion: self.appVersion, creationDate: self.creationDate, activityDate: self.activityDate, ip: self.ip, country: self.country, region: self.region, flags: flags) + } } extension RecentAccountSession { @@ -104,6 +115,9 @@ extension RecentAccountSession { if (flags & (1 << 3)) == 0 { accountSessionFlags.insert(.acceptsSecretChats) } + if (flags & (1 << 4)) == 0 { + accountSessionFlags.insert(.acceptsIncomingCalls) + } self.init(hash: hash, deviceModel: deviceModel, platform: platform, systemVersion: systemVersion, apiId: apiId, appName: appName, appVersion: appVersion, creationDate: dateCreated, activityDate: dateActive, ip: ip, country: country, region: region, flags: accountSessionFlags) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSessions.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSessions.swift index 7afdd56776..0d7b535aa7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSessions.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/RecentAccountSessions.swift @@ -78,3 +78,13 @@ func updateAccountSessionAcceptsSecretChats(account: Account, hash: Int64, accep return .single(Void()) } } + +func updateAccountSessionAcceptsIncomingCalls(account: Account, hash: Int64, accepts: Bool) -> Signal { + return account.network.request(Api.functions.account.changeAuthorizationSettings(flags: 1 << 1, hash: hash, encryptedRequestsDisabled: nil, callRequestsDisabled: accepts ? .boolFalse : .boolTrue)) + |> mapError { error -> UpdateSessionError in + return .generic + } + |> mapToSignal { _ -> Signal in + return .single(Void()) + } +} diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 3c3e3f247d..7f247ce276 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -162,6 +162,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case interactiveEmojiSyncTip = 28 case sharedMediaScrollingTooltip = 29 case sharedMediaFastScrollingTooltip = 30 + case forcedPasswordSetup = 31 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -211,6 +212,10 @@ private struct ApplicationSpecificNoticeKeys { return NoticeEntryKey(namespace: noticeNamespace(namespace: peerReportNamespace), key: noticeKey(peerId: peerId, key: 0)) } + static func forcedPasswordSetup() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.secretChatInlineBotUsage.key) + } + static func secretChatInlineBotUsage() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.secretChatInlineBotUsage.key) } @@ -1098,6 +1103,23 @@ public struct ApplicationSpecificNotice { } } + public static func forcedPasswordSetupKey() -> NoticeEntryKey { + return ApplicationSpecificNoticeKeys.forcedPasswordSetup() + } + + public static func setForcedPasswordSetup(postbox: Postbox, reloginDaysTimeout: Int32?) -> Signal { + return postbox.transaction { transaction -> Void in + if let reloginDaysTimeout = reloginDaysTimeout { + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: reloginDaysTimeout)) { + transaction.setNoticeEntry(key: ApplicationSpecificNoticeKeys.forcedPasswordSetup(), value: entry) + } + } else { + transaction.setNoticeEntry(key: ApplicationSpecificNoticeKeys.forcedPasswordSetup(), value: nil) + } + } + |> ignoreValues + } + public static func reset(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> Void in } diff --git a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift index 8ae310243c..2f64a23ff3 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationThemeEssentialGraphics.swift @@ -532,6 +532,7 @@ public final class PrincipalThemeAdditionalGraphics { public let chatBubbleActionButtonIncomingPhoneIconImage: UIImage public let chatBubbleActionButtonIncomingLocationIconImage: UIImage public let chatBubbleActionButtonIncomingPaymentIconImage: UIImage + public let chatBubbleActionButtonIncomingProfileIconImage: UIImage public let chatBubbleActionButtonOutgoingMessageIconImage: UIImage public let chatBubbleActionButtonOutgoingLinkIconImage: UIImage @@ -539,6 +540,7 @@ public final class PrincipalThemeAdditionalGraphics { public let chatBubbleActionButtonOutgoingPhoneIconImage: UIImage public let chatBubbleActionButtonOutgoingLocationIconImage: UIImage public let chatBubbleActionButtonOutgoingPaymentIconImage: UIImage + public let chatBubbleActionButtonOutgoingProfileIconImage: UIImage public let chatEmptyItemLockIcon: UIImage public let emptyChatListCheckIcon: UIImage @@ -581,12 +583,14 @@ public final class PrincipalThemeAdditionalGraphics { self.chatBubbleActionButtonIncomingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonIncomingPaymentIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPayment"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! + self.chatBubbleActionButtonIncomingProfileIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotProfile"), color: bubbleVariableColor(variableColor: theme.message.incoming.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingMessageIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotMessage"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingLinkIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLink"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingShareIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotShare"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingPhoneIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPhone"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingLocationIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotLocation"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatBubbleActionButtonOutgoingPaymentIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotPayment"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! + self.chatBubbleActionButtonOutgoingProfileIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotProfile"), color: bubbleVariableColor(variableColor: theme.message.outgoing.actionButtonsTextColor, wallpaper: wallpaper))! self.chatEmptyItemLockIcon = generateImage(CGSize(width: 9.0, height: 13.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/BotMessage@3x.png b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/BotMessage@3x.png new file mode 100644 index 0000000000..a896a845d6 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/BotMessage@3x.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/Contents.json new file mode 100644 index 0000000000..cd6ffbe035 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "BotMessage@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift b/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift index 663247d7fe..698aa9262f 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift @@ -177,8 +177,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.currentOptionNode.attributedText = authorizationCurrentOptionText(codeType, strings: self.strings, primaryColor: self.theme.list.itemPrimaryTextColor, accentColor: self.theme.list.itemAccentColor) if case .missedCall = codeType { - //TODO:localize - self.currentOptionInfoNode.attributedText = NSAttributedString(string: "Please enter the last five digits\nof the missed call number.", font: Font.regular(16.0), textColor: self.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) + self.currentOptionInfoNode.attributedText = NSAttributedString(string: self.strings.Login_CodePhonePatternInfoText, font: Font.regular(16.0), textColor: self.theme.list.itemPrimaryTextColor, paragraphAlignment: .center) } else { self.currentOptionInfoNode.attributedText = NSAttributedString(string: "", font: Font.regular(15.0), textColor: self.theme.list.itemPrimaryTextColor) } @@ -227,8 +226,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF case .otherSession: self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_CheckOtherSessionMessages, font: Font.medium(32.0), textColor: self.theme.list.itemPrimaryTextColor) case .missedCall: - //TODO:localize - self.titleNode.attributedText = NSAttributedString(string: "Enter the missing digits", font: Font.medium(32.0), textColor: self.theme.list.itemPrimaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterMissingDigits, font: Font.medium(32.0), textColor: self.theme.list.itemPrimaryTextColor) default: self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.light(40.0), textColor: self.theme.list.itemPrimaryTextColor) } @@ -253,8 +251,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF } else { fontSize = 18.0 } - //TODO:localize - self.titleNode.attributedText = NSAttributedString(string: "Enter the missing digits", font: Font.semibold(fontSize), textColor: self.theme.list.itemPrimaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: self.strings.Login_EnterMissingDigits, font: Font.semibold(fontSize), textColor: self.theme.list.itemPrimaryTextColor) default: self.titleNode.attributedText = NSAttributedString(string: self.phoneNumber, font: Font.light(30.0), textColor: self.theme.list.itemPrimaryTextColor) } diff --git a/submodules/TelegramUI/Sources/AuthorizationSequenceController.swift b/submodules/TelegramUI/Sources/AuthorizationSequenceController.swift index 2a0b71c431..a8f4256365 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequenceController.swift @@ -17,6 +17,7 @@ import PhoneNumberFormat import LegacyComponents import LegacyMediaPickerUI import PasswordSetupUI +import TelegramNotices private enum InnerState: Equatable { case state(UnauthorizedAccountStateContents) @@ -289,7 +290,12 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let strongSelf = self { controller?.inProgress = true - strongSelf.actionDisposable.set((authorizeWithCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, code: code, termsOfService: termsOfService?.0) + strongSelf.actionDisposable.set((authorizeWithCode(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, code: code, termsOfService: termsOfService?.0, forcedPasswordSetupNotice: { value in + guard let entry = CodableEntry(ApplicationSpecificCounterNotice(value: value)) else { + return nil + } + return (ApplicationSpecificNotice.forcedPasswordSetupKey(), entry) + }) |> deliverOnMainQueue).start(next: { result in guard let strongSelf = self else { return @@ -553,7 +559,7 @@ public final class AuthorizationSequenceController: NavigationController, MFMail if let currentController = currentController { controller = currentController } else { - controller = TwoFactorDataInputScreen(sharedContext: self.sharedContext, engine: .unauthorized(TelegramEngineUnauthorized(account: self.account)), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .notAuthorized(syncContacts: syncContacts)), stateUpdated: { _ in + controller = TwoFactorDataInputScreen(sharedContext: self.sharedContext, engine: .unauthorized(TelegramEngineUnauthorized(account: self.account)), mode: .passwordRecoveryEmail(emailPattern: emailPattern, mode: .notAuthorized(syncContacts: syncContacts), doneText: self.presentationData.strings.TwoFactorSetup_Done_Action), stateUpdated: { _ in }, presentation: .default) } controller.passwordRecoveryFailed = { [weak self] in @@ -713,7 +719,12 @@ public final class AuthorizationSequenceController: NavigationController, MFMail avatarVideo = nil } - strongSelf.actionDisposable.set((signUpWithName(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, firstName: firstName, lastName: lastName, avatarData: avatarData, avatarVideo: avatarVideo, videoStartTimestamp: videoStartTimestamp) + strongSelf.actionDisposable.set((signUpWithName(accountManager: strongSelf.sharedContext.accountManager, account: strongSelf.account, firstName: firstName, lastName: lastName, avatarData: avatarData, avatarVideo: avatarVideo, videoStartTimestamp: videoStartTimestamp, forcedPasswordSetupNotice: { value in + guard let entry = CodableEntry(ApplicationSpecificCounterNotice(value: value)) else { + return nil + } + return (ApplicationSpecificNotice.forcedPasswordSetupKey(), entry) + }) |> deliverOnMainQueue).start(error: { error in Queue.mainQueue().async { if let strongSelf = self, let controller = controller { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a5506c31aa..84775dd0ea 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8395,7 +8395,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.checksTooltipDisposable.set((getServerProvidedSuggestions(account: self.context.account) |> deliverOnMainQueue).start(next: { [weak self] values in - guard let strongSelf = self else { + guard let strongSelf = self, strongSelf.chatLocation.peerId != strongSelf.context.account.peerId else { return } if !values.contains(.newcomerTicks) { @@ -11864,16 +11864,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - //TODO:localize - var statusText: String - statusText = "Messages for \(dayCount) \(dayCount == 1 ? "day" : "days") deleted" + let statusText: String switch type { case .forEveryone: - statusText += " for both sides" + statusText = strongSelf.presentationData.strings.Chat_MessageRangeDeleted_ForBothSides(Int32(dayCount)) default: - break + statusText = strongSelf.presentationData.strings.Chat_MessageRangeDeleted_ForMe(Int32(dayCount)) } - statusText += "." strongSelf.chatDisplayNode.historyNode.ignoreMessagesInTimestampRange = range @@ -13570,7 +13567,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G tooltipController.dismissed = { [weak self, weak tooltipController] _ in if let strongSelf = self, let tooltipController = tooltipController, strongSelf.checksTooltipController === tooltipController { strongSelf.checksTooltipController = nil - // ApplicationSpecificNotice.setVolumeButtonToUnmute(accountManager: strongSelf.context.sharedContext.accountManager) } } self.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift index c3697c6713..ee7a76074c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift @@ -106,6 +106,8 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { iconImage = incoming ? graphics.chatBubbleActionButtonIncomingShareIconImage : graphics.chatBubbleActionButtonOutgoingShareIconImage case .payment: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPaymentIconImage : graphics.chatBubbleActionButtonOutgoingPaymentIconImage + case .openUserProfile: + iconImage = incoming ? graphics.chatBubbleActionButtonIncomingProfileIconImage : graphics.chatBubbleActionButtonOutgoingProfileIconImage default: iconImage = nil } diff --git a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift index 3260d9fde9..e616a208fb 100644 --- a/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatReportPeerTitlePanelNode.swift @@ -322,7 +322,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.closeButton.displaysAsynchronously = false self.textNode = ImmediateTextNode() - self.textNode.maximumNumberOfLines = 2 + self.textNode.maximumNumberOfLines = 3 self.textNode.textAlignment = .center super.init() diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 04fa0b309a..2a0a93a01b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -5496,7 +5496,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } case .rememberPassword: let context = self.context - let controller = TwoFactorDataInputScreen(sharedContext: self.context.sharedContext, engine: .authorized(self.context.engine), mode: .rememberPassword, stateUpdated: { _ in + let controller = TwoFactorDataInputScreen(sharedContext: self.context.sharedContext, engine: .authorized(self.context.engine), mode: .rememberPassword(doneText: self.presentationData.strings.TwoFactorSetup_Done_Action), stateUpdated: { _ in }, presentation: .modalInLargeLayout) controller.twoStepAuthSettingsController = { configuration in return twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: .single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: configuration, password: nil))))) diff --git a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift index 8552a32d3d..e078994d54 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionTextInputPanelNode.swift @@ -716,7 +716,13 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A } private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { - if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { + let inputTextMaxLength: Int32? + if self.isCaption { + inputTextMaxLength = self.context?.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength + } else { + inputTextMaxLength = nil + } + if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let inputTextMaxLength = inputTextMaxLength { let textCount = Int32(textInputNode.textView.text.count) let counterColor: UIColor = textCount > inputTextMaxLength ? presentationInterfaceState.theme.chat.inputPanel.panelControlDestructiveColor : presentationInterfaceState.theme.chat.inputPanel.panelControlColor @@ -1028,7 +1034,13 @@ class PeerSelectionTextInputPanelNode: ChatInputPanelNode, TGCaptionPanelView, A sendPressed(effectiveInputText) return } - if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { + let inputTextMaxLength: Int32? + if self.isCaption { + inputTextMaxLength = self.context?.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength + } else { + inputTextMaxLength = nil + } + if let textInputNode = self.textInputNode, let inputTextMaxLength = inputTextMaxLength { let textCount = Int32(textInputNode.textView.text.count) let remainingCount = inputTextMaxLength - textCount diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 8e1dba89fd..73e2effe84 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -65,6 +65,7 @@ private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { case mediaPlaybackStoredState = 3 case cachedGeocodes = 4 case visualMediaStoredState = 5 + case cachedImageRecognizedContent = 6 } public struct ApplicationSpecificItemCacheCollectionId { @@ -74,6 +75,7 @@ public struct ApplicationSpecificItemCacheCollectionId { public static let mediaPlaybackStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.mediaPlaybackStoredState.rawValue) public static let cachedGeocodes = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedGeocodes.rawValue) public static let visualMediaStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.visualMediaStoredState.rawValue) + public static let cachedImageRecognizedContent = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedImageRecognizedContent.rawValue) } private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { @@ -81,7 +83,6 @@ private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { case wallpaperSearchRecentQueries = 1 case settingsSearchRecentItems = 2 case localThemes = 3 - case visualMediaStoredState = 4 } public struct ApplicationSpecificOrderedItemListCollectionId {