mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' into beta
This commit is contained in:
commit
e4cb4aea73
@ -3765,6 +3765,7 @@ Unused sets are archived when you add more.";
|
||||
"AuthCode.Alert" = "Your login code is %@. Enter it in the Telegram app where you are trying to log in.\n\nDo not give this code to anyone.";
|
||||
"Login.CheckOtherSessionMessages" = "Check your Telegram messages";
|
||||
"Login.SendCodeViaSms" = "Send the code as an SMS";
|
||||
"Login.SendCodeViaCall" = "Get code on my phone";
|
||||
"Login.CancelPhoneVerification" = "Do you want to stop the phone number verification process?";
|
||||
"Login.CancelPhoneVerificationStop" = "Stop";
|
||||
"Login.CancelPhoneVerificationContinue" = "Continue";
|
||||
@ -7079,9 +7080,9 @@ Sorry for the inconvenience.";
|
||||
|
||||
"AuthSessions.View.LocationInfo" = "This location estimate is based on the IP address and may not always be accurate.";
|
||||
|
||||
"AuthSessions.View.AcceptSecretChatsTitle" = "Incoming Secret Chats";
|
||||
"AuthSessions.View.AcceptSecretChats" = "Accept on This Device";
|
||||
"AuthSessions.View.AcceptSecretChatsInfo" = "You can disable the acception of incoming secret chats on this device.";
|
||||
"AuthSessions.View.AcceptTitle" = "Accept on This Device";
|
||||
"AuthSessions.View.AcceptSecretChats" = "New Secret Chats";
|
||||
"AuthSessions.View.AcceptIncomingCalls" = "Incoming Calls";
|
||||
|
||||
"Conversation.SendMesageAs" = "Send Message As...";
|
||||
"Conversation.InviteRequestAdminGroup" = "%1$@ is an admin of %2$@, a group you requested to join.";
|
||||
@ -7101,3 +7102,24 @@ Sorry for the inconvenience.";
|
||||
"AuthSessions.TerminateOtherSessionsText" = "Are you sure you want to terminate all other sessions?";
|
||||
|
||||
"Notifications.ResetAllNotificationsText" = "Are you sure you want to reset all notification settings to default?";
|
||||
|
||||
"MessageCalendar.ClearHistoryForThisDay" = "Clear History For This Day";
|
||||
"MessageCalendar.ClearHistoryForTheseDays" = "Clear History For These Days";
|
||||
"MessageCalendar.EmptySelectionTooltip" = "Please select one or more days first.";
|
||||
"Chat.MessageRangeDeleted.ForMe_1" = "Messages for 1 day deleted.";
|
||||
"Chat.MessageRangeDeleted.ForMe_any" = "Messages for %@ days deleted.";
|
||||
"Chat.MessageRangeDeleted.ForBothSides_1" = "Messages for 1 day deleted for both sides.";
|
||||
"Chat.MessageRangeDeleted.ForBothSides_any" = "Messages for %@ days deleted for both sides.";
|
||||
|
||||
"ForcedPasswordSetup.Intro.Title" = "Set a Password";
|
||||
"ForcedPasswordSetup.Intro.Text" = "If you want to log into your account frequently, please choose a password.";
|
||||
"ForcedPasswordSetup.Intro.Action" = "Set a Password";
|
||||
"ForcedPasswordSetup.Intro.DoneAction" = "Done";
|
||||
"ForcedPasswordSetup.Intro.DismissTitle" = "Warning";
|
||||
"ForcedPasswordSetup.Intro.DismissText_1" = "Proceed without a password? If you do not set a password, you will only be able to log into your account via SMS once every **day**.";
|
||||
"ForcedPasswordSetup.Intro.DismissText_any" = "Proceed without a password? If you do not set a password, you will only be able to log into your account via SMS once every **%@ days**.";
|
||||
"ForcedPasswordSetup.Intro.DismissActionCancel" = "No, let me set a password";
|
||||
"ForcedPasswordSetup.Intro.DismissActionOK" = "Yes, I’m sure";
|
||||
|
||||
"Login.CodePhonePatternInfoText" = "Please enter the last digits\nof the missed call number.";
|
||||
"Login.EnterMissingDigits" = "Enter the missing digits";
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ swift_library(
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/DirectMediaImageCache:DirectMediaImageCache",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
540
submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift
Normal file
540
submodules/GalleryUI/Sources/RecognizedTextSelectionNode.swift
Normal 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
|
||||
}
|
||||
}
|
22
submodules/ImageContentAnalysis/BUILD
Normal file
22
submodules/ImageContentAnalysis/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/BotMessage@3x.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/BotMessage@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
21
submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Chat/Message/BotProfile.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)))))
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user