Merge branch 'master' into beta

This commit is contained in:
Ali 2021-11-27 00:04:51 +04:00
commit e4cb4aea73
41 changed files with 1766 additions and 192 deletions

View File

@ -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, Im sure";
"Login.CodePhonePatternInfoText" = "Please enter the last digits\nof the missed call number.";
"Login.EnterMissingDigits" = "Enter the missing digits";

View File

@ -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)
}

View File

@ -21,6 +21,7 @@ swift_library(
"//submodules/PhotoResources:PhotoResources",
"//submodules/DirectMediaImageCache:DirectMediaImageCache",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TooltipUI:TooltipUI",
],
visibility = [
"//visibility:public",

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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",

View File

@ -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()

View File

@ -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) {

View File

@ -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 {

View File

@ -58,6 +58,10 @@ open class GalleryItemNode: ASDisplayNode {
return .single(nil)
}
open func isPagingEnabled() -> Signal<Bool, NoError> {
return .single(true)
}
open func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((nil, nil))
}

View File

@ -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<Bool>(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

View File

@ -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<Void>()
fileprivate let _title = Promise<String>()
@ -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<Bool>(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<Bool, NoError> {
return self.pagingEnabledPromise.get()
}
deinit {
//self.fetchDisposable.dispose()
self.statusDisposable.dispose()
self.dataDisposable.dispose()
self.recognitionDisposable.dispose()
}
override func ready() -> Signal<Void, NoError> {
@ -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
}
}
}

View File

@ -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))
}
}

View File

@ -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<UITouch>, 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<UITouch>, 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<UITouch>, 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<UITouch>, 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<Int>?
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<Int>?
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<Int>?
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<Int>?, 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
}
}

View File

@ -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",
],
)

View File

@ -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<CachedImageRecognizedContent?, NoError> {
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<Void, NoError> {
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<String.Index>, 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<String.Index>, 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<String.Index>, 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<String.Index>, 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()
}
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -105,11 +105,11 @@ public func sendAuthorizationCode(accountManager: AccountManager<TelegramAccount
}
|> `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<TelegramAccountManagerTypes>, account: UnauthorizedAccount, code: String, termsOfService: UnauthorizedAccountTermsOfService?) -> Signal<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> {
public func authorizeWithCode(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, code: String, termsOfService: UnauthorizedAccountTermsOfService?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> {
return account.postbox.transaction { transaction -> Signal<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> in
if let state = transaction.getState() as? UnauthorizedAccountState {
switch state.contents {
@ -302,11 +302,14 @@ public func authorizeWithCode(accountManager: AccountManager<TelegramAccountMana
return .single(.loggedIn)
case let .authorization(authorization):
switch authorization {
case let .authorization(_, _, _, user):
case let .authorization(_, otherwiseReloginDays, _, user):
let user = TelegramUser(user: user)
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) {
transaction.setNoticeEntry(key: value.0, value: value.1)
}
return accountManager.transaction { transaction -> AuthorizeWithCodeResult in
switchToAuthorizedAccount(transaction: transaction, account: account)
return .loggedIn
@ -542,7 +545,7 @@ public enum SignUpError {
case invalidLastName
}
public func signUpWithName(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, firstName: String, lastName: String, avatarData: Data?, avatarVideo: Signal<UploadedPeerPhotoData?, NoError>?, videoStartTimestamp: Double?) -> Signal<Void, SignUpError> {
public func signUpWithName(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, firstName: String, lastName: String, avatarData: Data?, avatarVideo: Signal<UploadedPeerPhotoData?, NoError>?, videoStartTimestamp: Double?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal<Void, SignUpError> {
return account.postbox.transaction { transaction -> Signal<Void, SignUpError> 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<TelegramAccountManager
}
|> mapToSignal { result -> Signal<Void, SignUpError> 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<TelegramAccountManager
}
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) {
transaction.setNoticeEntry(key: value.0, value: value.1)
}
|> castError(SignUpError.self)
}
|> castError(SignUpError.self)
let switchedAccounts = accountManager.transaction { transaction -> Void in
switchToAuthorizedAccount(transaction: transaction, account: account)

View File

@ -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]
}
}

View File

@ -144,6 +144,29 @@ private final class ActiveSessionsContextImpl {
}
}
func updateSessionAcceptsIncomingCalls(_ session: RecentAccountSession, accepts: Bool) -> Signal<Never, UpdateSessionError> {
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<Never, UpdateSessionError> 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<Never, UpadteAuthorizationTTLError> {
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<Never, UpdateSessionError> {
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<Never, UpadteAuthorizationTTLError> {
let days = max(1, min(365, days))
return Signal { subscriber in

View File

@ -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)
}
}

View File

@ -78,3 +78,13 @@ func updateAccountSessionAcceptsSecretChats(account: Account, hash: Int64, accep
return .single(Void())
}
}
func updateAccountSessionAcceptsIncomingCalls(account: Account, hash: Int64, accepts: Bool) -> Signal<Void, UpdateSessionError> {
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<Void, UpdateSessionError> in
return .single(Void())
}
}

View File

@ -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<Never, NoError> {
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<TelegramAccountManagerTypes>) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in
}

View File

@ -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))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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()

View File

@ -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)))))

View File

@ -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

View File

@ -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 {