From 39ceca96aa8c511855f9fa5f2d4bc9f6094695df Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 9 Dec 2019 19:35:43 +0400 Subject: [PATCH] Improve QR auth and sessions UI --- submodules/AuthTransferUI/BUCK | 1 + .../AuthTransferConfirmationScreen.swift | 116 +++++++++++++++- .../MediaPlayer/Sources/MediaPlayerNode.swift | 125 ++++++++++++++++-- .../PrivacyAndSecurityController.swift | 5 +- .../RecentSessionsController.swift | 58 +++----- .../Search/SettingsSearchableItems.swift | 2 +- .../Sources/SettingsController.swift | 15 ++- .../Sources/ActiveSessionsContext.swift | 108 +++++++++++++++ .../ChatInterfaceStateContextQueries.swift | 2 +- .../EmojisChatInputContextPanelNode.swift | 13 +- 10 files changed, 366 insertions(+), 79 deletions(-) diff --git a/submodules/AuthTransferUI/BUCK b/submodules/AuthTransferUI/BUCK index c2b825584a..690bff6fc3 100644 --- a/submodules/AuthTransferUI/BUCK +++ b/submodules/AuthTransferUI/BUCK @@ -24,6 +24,7 @@ static_library( "//submodules/Markdown:Markdown", "//submodules/AnimationUI:AnimationUI", "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/DeviceAccess:DeviceAccess", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift b/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift index c25ce9d207..205c4da0e6 100644 --- a/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift +++ b/submodules/AuthTransferUI/Sources/AuthTransferConfirmationScreen.swift @@ -12,6 +12,68 @@ import TelegramPresentationData import PresentationDataUtils import TelegramCore import Markdown +import DeviceAccess + +private let colorKeyRegex = try? NSRegularExpression(pattern: "\"k\":\\[[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\]") + +private func transformedWithTheme(data: Data, theme: PresentationTheme) -> Data { + if var string = String(data: data, encoding: .utf8) { + var colors: [UIColor] = [0x333333, 0xFFFFFF, 0x50A7EA, 0x212121].map { UIColor(rgb: $0) } + let replacementColors: [UIColor] = [theme.list.itemPrimaryTextColor.mixedWith(.white, alpha: 0.2), theme.list.plainBackgroundColor, theme.list.itemAccentColor, theme.list.itemPrimaryTextColor.mixedWith(.white, alpha: 0.12)] + + func colorToString(_ color: UIColor) -> String { + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + if color.getRed(&r, green: &g, blue: &b, alpha: nil) { + return "\"k\":[\(r),\(g),\(b),1]" + } + return "" + } + + func match(_ a: Double, _ b: Double, eps: Double) -> Bool { + return abs(a - b) < eps + } + + var replacements: [(NSTextCheckingResult, String)] = [] + + if let colorKeyRegex = colorKeyRegex { + let results = colorKeyRegex.matches(in: string, range: NSRange(string.startIndex..., in: string)) + for result in results.reversed() { + if let range = Range(result.range, in: string) { + let substring = String(string[range]) + let color = substring[substring.index(string.startIndex, offsetBy: "\"k\":[".count) ..< substring.index(before: substring.endIndex)] + let components = color.split(separator: ",") + if components.count == 4, let r = Double(components[0]), let g = Double(components[1]), let b = Double(components[2]), let a = Double(components[3]) { + if match(a, 1.0, eps: 0.01) { + for i in 0 ..< colors.count { + let color = colors[i] + var cr: CGFloat = 0.0 + var cg: CGFloat = 0.0 + var cb: CGFloat = 0.0 + if color.getRed(&cr, green: &cg, blue: &cb, alpha: nil) { + if match(r, Double(cr), eps: 0.01) && match(g, Double(cg), eps: 0.01) && match(b, Double(cb), eps: 0.01) { + replacements.append((result, colorToString(replacementColors[i]))) + } + } + } + } + } + } + } + } + + for (result, text) in replacements { + if let range = Range(result.range, in: string) { + string = string.replacingCharacters(in: range, with: text) + } + } + + return string.data(using: .utf8) ?? data + } else { + return data + } +} public final class AuthDataTransferSplashScreen: ViewController { private let context: AccountContext @@ -49,7 +111,24 @@ public final class AuthDataTransferSplashScreen: ViewController { guard let strongSelf = self else { return } - (strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: AuthTransferScanScreen(context: strongSelf.context, activeSessionsContext: strongSelf.activeSessionsContext), animated: true) + + DeviceAccess.authorizeAccess(to: .camera, presentationData: strongSelf.presentationData, present: { c, a in + guard let strongSelf = self else { + return + } + c.presentationArguments = a + strongSelf.context.sharedContext.mainWindow?.present(c, on: .root) + }, openSettings: { + self?.context.sharedContext.applicationBindings.openSettings() + }, { granted in + guard let strongSelf = self else { + return + } + guard granted else { + return + } + (strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: AuthTransferScanScreen(context: strongSelf.context, activeSessionsContext: strongSelf.activeSessionsContext), animated: true) + }) }) self.displayNodeDidLoad() @@ -67,13 +146,15 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode private var animationSize: CGSize = CGSize() private var animationOffset: CGPoint = CGPoint() - private let animationNode: AnimationNode + private let animationNode: AnimationNode? private let titleNode: ImmediateTextNode private let badgeBackgroundNodes: [ASImageNode] private let badgeTextNodes: [ImmediateTextNode] private let textNodes: [ImmediateTextNode] let buttonNode: SolidRoundedButtonNode + private let hierarchyTrackingNode: HierarchyTrackingNode + var inProgress: Bool = false { didSet { self.buttonNode.isUserInteractionEnabled = !self.inProgress @@ -86,7 +167,11 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode init(context: AccountContext, presentationData: PresentationData, action: @escaping () -> Void) { self.presentationData = presentationData - self.animationNode = AnimationNode(animation: "anim_qr", colors: nil, scale: UIScreenScale) + if let url = getAppBundle().url(forResource: "anim_qr", withExtension: "json"), let data = try? Data(contentsOf: url) { + self.animationNode = AnimationNode(animationData: transformedWithTheme(data: data, theme: presentationData.theme)) + } else { + self.animationNode = nil + } let buttonText: String @@ -154,11 +239,20 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor), height: 50.0, cornerRadius: 10.0, gloss: false) self.buttonNode.isHidden = buttonText.isEmpty + var updateInHierarchy: ((Bool) -> Void)? + self.hierarchyTrackingNode = HierarchyTrackingNode({ value in + updateInHierarchy?(value) + }) + super.init() self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - self.addSubnode(self.animationNode) + self.addSubnode(self.hierarchyTrackingNode) + + if let animationNode = self.animationNode { + self.addSubnode(animationNode) + } self.addSubnode(self.titleNode) self.badgeBackgroundNodes.forEach(self.addSubnode) @@ -186,6 +280,12 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode } } } + + updateInHierarchy = { [weak self] value in + if value { + self?.animationNode?.play() + } + } } override func didLoad() { @@ -206,7 +306,7 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode let badgeSize: CGFloat = 20.0 let animationFitSize = CGSize(width: min(500.0, layout.size.width - sideInset + 20.0), height: 500.0) - let animationSize = self.animationNode.preferredSize()?.fitted(animationFitSize) ?? animationFitSize + let animationSize = self.animationNode?.preferredSize()?.fitted(animationFitSize) ?? animationFitSize let iconSize: CGSize = animationSize var iconOffset = CGPoint() @@ -252,7 +352,9 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode var contentY = contentVerticalOrigin let iconFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0) + self.animationOffset.x, y: contentY), size: iconSize).offsetBy(dx: iconOffset.x, dy: iconOffset.y) contentY += iconSize.height + iconSpacing - transition.updateFrameAdditive(node: self.animationNode, frame: iconFrame) + if let animationNode = self.animationNode { + transition.updateFrameAdditive(node: animationNode, frame: iconFrame) + } let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: contentY), size: titleSize) transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) @@ -285,7 +387,7 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode } if firstTime { - self.animationNode.play() + self.animationNode?.play() } } } diff --git a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift index 40be380263..220adfb6a1 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerNode.swift @@ -146,14 +146,117 @@ public final class MediaPlayerNode: ASDisplayNode { private func poll(completion: @escaping (PollStatus) -> Void) { if let (takeFrameQueue, takeFrame) = self.takeFrameAndQueue, let videoLayer = self.videoLayer, let (timebase, _, _, _) = self.state { - let layerRef = Unmanaged.passRetained(videoLayer) + let layerTime = CMTimeGetSeconds(CMTimebaseGetTime(timebase)) + let rate = CMTimebaseGetRate(timebase) + + struct PollState { + var numFrames: Int + var maxTakenTime: Double + } + + var loop: ((PollState) -> Void)? + let loopImpl: (PollState) -> Void = { [weak self] state in + assert(Queue.mainQueue().isCurrent()) + + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + if !videoLayer.isReadyForMoreMediaData { + completion(.delay(max(1.0 / 30.0, state.maxTakenTime - layerTime))) + return + } + + var state = state + + takeFrameQueue.async { + switch takeFrame() { + case let .restoreState(frames, atTime): + Queue.mainQueue().async { + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.flush() + } + for i in 0 ..< frames.count { + let frame = frames[i] + let frameTime = CMTimeGetSeconds(frame.position) + state.maxTakenTime = frameTime + let attachments = CMSampleBufferGetSampleAttachmentsArray(frame.sampleBuffer, createIfNecessary: true)! as NSArray + let dict = attachments[0] as! NSMutableDictionary + if i == 0 { + CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) + CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) + } + if CMTimeCompare(frame.position, atTime) < 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String) + } else if CMTimeCompare(frame.position, atTime) == 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString as String) + } + Queue.mainQueue().async { + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.enqueue(frame.sampleBuffer) + } + } + Queue.mainQueue().async { + loop?(state) + } + case let .frame(frame): + state.numFrames += 1 + let frameTime = CMTimeGetSeconds(frame.position) + if frame.resetDecoder { + Queue.mainQueue().async { + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.flush() + } + } + + if frame.decoded && frameTime < layerTime { + Queue.mainQueue().async { + loop?(state) + } + } else { + state.maxTakenTime = frameTime + Queue.mainQueue().async { + guard let strongSelf = self, let videoLayer = strongSelf.videoLayer else { + return + } + videoLayer.enqueue(frame.sampleBuffer) + } + + Queue.mainQueue().async { + loop?(state) + } + } + case .skipFrame: + Queue.mainQueue().async { + loop?(state) + } + case .noFrames: + DispatchQueue.main.async { + completion(.finished) + } + case .finished: + DispatchQueue.main.async { + completion(.finished) + } + } + } + } + loop = loopImpl + loop?(PollState(numFrames: 0, maxTakenTime: layerTime + 0.1)) + + /*let layerRef = Unmanaged.passRetained(videoLayer) takeFrameQueue.async { let status: PollStatus do { var numFrames = 0 let layer = layerRef.takeUnretainedValue() - let layerTime = CMTimeGetSeconds(CMTimebaseGetTime(timebase)) - let rate = CMTimebaseGetRate(timebase) + var maxTakenTime = layerTime + 0.1 var finised = false loop: while true { @@ -230,7 +333,7 @@ public final class MediaPlayerNode: ASDisplayNode { completion(status) } - } + }*/ } } @@ -286,10 +389,6 @@ public final class MediaPlayerNode: ASDisplayNode { strongSelf.layer.addSublayer(videoLayer) - /*let testLayer = RuntimeUtils.makeLayerHostCopy(videoLayer.sublayers![0].sublayers![0])*/ - //testLayer.frame = CGRect(origin: CGPoint(x: -500.0, y: -300.0), size: CGSize(width: 60.0, height: 60.0)) - //strongSelf.layer.addSublayer(testLayer) - strongSelf.updateState() } } @@ -302,13 +401,11 @@ public final class MediaPlayerNode: ASDisplayNode { if let (takeFrameQueue, _) = self.takeFrameAndQueue { if let videoLayer = self.videoLayer { - takeFrameQueue.async { + videoLayer.flushAndRemoveImage() + + Queue.mainQueue().after(1.0, { videoLayer.flushAndRemoveImage() - - takeFrameQueue.after(1.0, { - videoLayer.flushAndRemoveImage() - }) - } + }) } } } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index b38d0d8319..123386d900 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -427,7 +427,7 @@ private func privacyAndSecurityControllerEntries(presentationData: PresentationD return entries } -public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil) -> ViewController { +public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil, webSessionsContext: WebSessionsContext? = nil) -> ViewController { let statePromise = ValuePromise(PrivacyAndSecurityControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: PrivacyAndSecurityControllerState()) let updateState: ((PrivacyAndSecurityControllerState) -> PrivacyAndSecurityControllerState) -> Void = { f in @@ -451,6 +451,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting let blockedPeersContext = BlockedPeersContext(account: context.account) let activeSessionsContext = activeSessionsContext ?? ActiveSessionsContext(account: context.account) + let webSessionsContext = webSessionsContext ?? WebSessionsContext(account: context.account) let updateTwoStepAuthDisposable = MetaDisposable() actionsDisposable.add(updateTwoStepAuthDisposable) @@ -669,7 +670,7 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting pushControllerImpl?(controller, true) } }, openActiveSessions: { - pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext), true) + pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext), true) }, setupAccountAutoremove: { let signal = privacySettingsPromise.get() |> take(1) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index 20dcb44472..76be9a46c1 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -447,7 +447,7 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, return entries } -public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController { +public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext) -> ViewController { let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: RecentSessionsControllerState()) let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in @@ -455,6 +455,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } activeSessionsContext.loadMore() + webSessionsContext.loadMore() var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? @@ -545,33 +546,11 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont return $0.withUpdatedRemovingSessionId(sessionId) } - let applySessions: Signal = websitesPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { websitesAndPeers -> Signal in - if let websites = websitesAndPeers?.0, let peers = websitesAndPeers?.1 { - var updatedWebsites = websites - for i in 0 ..< updatedWebsites.count { - if updatedWebsites[i].hash == sessionId { - updatedWebsites.remove(at: i) - break - } - } - - if updatedWebsites.isEmpty { - mode.set(.sessions) - } - websitesPromise.set(.single((updatedWebsites, peers))) - } - - return .complete() - } - - removeSessionDisposable.set(((terminateWebSession(network: context.account.network, hash: sessionId) - |> mapToSignal { _ -> Signal in - return .complete() - }) |> then(applySessions) |> deliverOnMainQueue).start(error: { _ in + removeSessionDisposable.set(((webSessionsContext.remove(hash: sessionId) + |> mapToSignal { _ -> Signal in + return .complete() + }) + |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingSessionId(nil) } @@ -595,7 +574,8 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont return $0.withUpdatedTerminatingOtherSessions(true) } - terminateOtherSessionsDisposable.set((terminateAllWebSessions(network: context.account.network) |> deliverOnMainQueue).start(error: { _ in + terminateOtherSessionsDisposable.set((webSessionsContext.removeAll() + |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedTerminatingOtherSessions(false) } @@ -604,10 +584,9 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont return $0.withUpdatedTerminatingOtherSessions(false) } mode.set(.sessions) - websitesPromise.set(.single(([], [:]))) })) }) - ]), + ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) @@ -617,9 +596,6 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://telegram.org/desktop", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) }) - let websitesSignal: Signal<([WebAuthorization], [PeerId : Peer])?, NoError> = .single(nil) |> then(webSessions(network: context.account.network) |> map(Optional.init)) - websitesPromise.set(websitesSignal) - let previousMode = Atomic(value: .sessions) let enableQRLogin = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) @@ -634,12 +610,12 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } |> distinctUntilChanged - let signal = combineLatest(context.sharedContext.presentationData, mode.get(), statePromise.get(), activeSessionsContext.state, websitesPromise.get(), enableQRLogin) + let signal = combineLatest(context.sharedContext.presentationData, mode.get(), statePromise.get(), activeSessionsContext.state, webSessionsContext.state, enableQRLogin) |> deliverOnMainQueue |> map { presentationData, mode, state, sessionsState, websitesAndPeers, enableQRLogin -> (ItemListControllerState, (ItemListNodeState, Any)) in var rightNavigationButton: ItemListNavigationButton? - let websites = websitesAndPeers?.0 - let peers = websitesAndPeers?.1 + let websites = websitesAndPeers.sessions + let peers = websitesAndPeers.peers if sessionsState.sessions.count > 1 { if state.terminatingOtherSessions { @@ -659,16 +635,16 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } } - var emptyStateItem: ItemListControllerEmptyStateItem? - if sessionsState.sessions.isEmpty { + let emptyStateItem: ItemListControllerEmptyStateItem? = nil + /*if sessionsState.sessions.isEmpty { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } else if sessionsState.sessions.count == 1 && mode == .sessions { emptyStateItem = RecentSessionsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings) - } + }*/ let title: ItemListControllerTitle let entries: [RecentSessionsEntry] - if let websites = websites, !websites.isEmpty { + if !websites.isEmpty { title = .sectionControl([presentationData.strings.AuthSessions_Sessions, presentationData.strings.AuthSessions_LoggedIn], mode.rawValue) } else { title = .text(presentationData.strings.AuthSessions_DevicesTitle) diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index de46ee2e42..b76289ecd1 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -545,7 +545,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac present(.push, twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: true, data: nil))) }), SettingsSearchableItem(id: .privacy(9), title: strings.PrivacySettings_AuthSessions, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_AuthSessions), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - present(.push, recentSessionsController(context: context, activeSessionsContext: ActiveSessionsContext(account: context.account))) + present(.push, recentSessionsController(context: context, activeSessionsContext: ActiveSessionsContext(account: context.account), webSessionsContext: WebSessionsContext(account: context.account))) }), SettingsSearchableItem(id: .privacy(10), title: strings.PrivacySettings_DeleteAccountTitle, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in presentPrivacySettings(context, present, .accountTimeout) diff --git a/submodules/SettingsUI/Sources/SettingsController.swift b/submodules/SettingsUI/Sources/SettingsController.swift index 7b1bf3bcd0..0894a604c1 100644 --- a/submodules/SettingsUI/Sources/SettingsController.swift +++ b/submodules/SettingsUI/Sources/SettingsController.swift @@ -859,8 +859,9 @@ public func settingsController(context: AccountContext, accountManager: AccountM let activeSessionsContextAndCountSignal = contextValue.get() |> deliverOnMainQueue - |> mapToSignal { context -> Signal<(ActiveSessionsContext, Int), NoError> in + |> mapToSignal { context -> Signal<(ActiveSessionsContext, Int, WebSessionsContext), NoError> in let activeSessionsContext = ActiveSessionsContext(account: context.account) + let webSessionsContext = WebSessionsContext(account: context.account) let otherSessionCount = activeSessionsContext.state |> map { state -> Int in return state.sessions.filter({ !$0.isCurrent }).count @@ -868,10 +869,10 @@ public func settingsController(context: AccountContext, accountManager: AccountM |> distinctUntilChanged return otherSessionCount |> map { value in - return (activeSessionsContext, value) + return (activeSessionsContext, value, webSessionsContext) } } - let activeSessionsContextAndCount = Promise<(ActiveSessionsContext, Int)>() + let activeSessionsContextAndCount = Promise<(ActiveSessionsContext, Int, WebSessionsContext)>() activeSessionsContextAndCount.set(activeSessionsContextAndCountSignal) let arguments = SettingsItemArguments(sharedContext: context.sharedContext, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { @@ -933,10 +934,10 @@ public func settingsController(context: AccountContext, accountManager: AccountM |> take(1)).start(next: { context in let _ = (activeSessionsContextAndCount.get() |> deliverOnMainQueue - |> take(1)).start(next: { activeSessionsContext, _ in + |> take(1)).start(next: { activeSessionsContext, _, webSessionsContext in pushControllerImpl?(privacyAndSecurityController(context: context, initialSettings: privacySettingsValue, updatedSettings: { settings in privacySettings.set(.single(settings)) - }, activeSessionsContext: activeSessionsContext)) + }, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext)) }) }) }, openDataAndStorage: { @@ -1108,11 +1109,11 @@ public func settingsController(context: AccountContext, accountManager: AccountM }, openDevices: { let _ = (activeSessionsContextAndCount.get() |> deliverOnMainQueue - |> take(1)).start(next: { activeSessionsContext, count in + |> take(1)).start(next: { activeSessionsContext, count, webSessionsContext in if count == 0 { pushControllerImpl?(AuthDataTransferSplashScreen(context: context, activeSessionsContext: activeSessionsContext)) } else { - pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext)) + pushControllerImpl?(recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext)) } }) }) diff --git a/submodules/TelegramCore/Sources/ActiveSessionsContext.swift b/submodules/TelegramCore/Sources/ActiveSessionsContext.swift index 7beeaba0a8..cfe51296a1 100644 --- a/submodules/TelegramCore/Sources/ActiveSessionsContext.swift +++ b/submodules/TelegramCore/Sources/ActiveSessionsContext.swift @@ -97,3 +97,111 @@ public final class ActiveSessionsContext { } } } + +public struct WebSessionsContextState: Equatable { + public var isLoadingMore: Bool + public var sessions: [WebAuthorization] + public var peers: [PeerId: Peer] + + public static func ==(lhs: WebSessionsContextState, rhs: WebSessionsContextState) -> Bool { + if lhs.isLoadingMore != rhs.isLoadingMore { + return false + } + if lhs.sessions != rhs.sessions { + return false + } + if !arePeerDictionariesEqual(lhs.peers, rhs.peers) { + return false + } + return true + } +} + +public final class WebSessionsContext { + private let account: Account + private var _state: WebSessionsContextState { + didSet { + if self._state != oldValue { + self._statePromise.set(.single(self._state)) + } + } + } + private let _statePromise = Promise() + public var state: Signal { + return self._statePromise.get() + } + + private let disposable = MetaDisposable() + + public init(account: Account) { + assert(Queue.mainQueue().isCurrent()) + + self.account = account + self._state = WebSessionsContextState(isLoadingMore: false, sessions: [], peers: [:]) + self._statePromise.set(.single(self._state)) + + self.loadMore() + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.disposable.dispose() + } + + public func loadMore() { + assert(Queue.mainQueue().isCurrent()) + + if self._state.isLoadingMore { + return + } + self._state = WebSessionsContextState(isLoadingMore: true, sessions: self._state.sessions, peers: self._state.peers) + self.disposable.set((webSessions(network: account.network) + |> map { result -> (sessions: [WebAuthorization], peers: [PeerId: Peer], canLoadMore: Bool) in + return (result.0, result.1, false) + } + |> deliverOnMainQueue).start(next: { [weak self] (sessions, peers, canLoadMore) in + guard let strongSelf = self else { + return + } + + strongSelf._state = WebSessionsContextState(isLoadingMore: false, sessions: sessions, peers: peers) + })) + } + + public func remove(hash: Int64) -> Signal { + assert(Queue.mainQueue().isCurrent()) + + return terminateWebSession(network: self.account.network, hash: hash) + |> deliverOnMainQueue + |> mapToSignal { [weak self] _ -> Signal in + guard let strongSelf = self else { + return .complete() + } + + var mergedSessions = strongSelf._state.sessions + for i in 0 ..< mergedSessions.count { + if mergedSessions[i].hash == hash { + mergedSessions.remove(at: i) + break + } + } + + strongSelf._state = WebSessionsContextState(isLoadingMore: strongSelf._state.isLoadingMore, sessions: mergedSessions, peers: strongSelf._state.peers) + return .complete() + } + } + + public func removeAll() -> Signal { + return terminateAllWebSessions(network: self.account.network) + |> deliverOnMainQueue + |> mapToSignal { [weak self] _ -> Signal in + guard let strongSelf = self else { + return .complete() + } + + strongSelf._state = WebSessionsContextState(isLoadingMore: strongSelf._state.isLoadingMore, sessions: [], peers: [:]) + return .complete() + } + } +} + diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextQueries.swift index 57b4e5930a..125cf85916 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -288,7 +288,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee return signal |> then(contextBot) case let .emojiSearch(query, languageCode, range): - var signal = searchEmojiKeywords(postbox: context.account.postbox, inputLanguageCode: languageCode, query: query, completeMatch: query.count < 3) + var signal = searchEmojiKeywords(postbox: context.account.postbox, inputLanguageCode: languageCode, query: query, completeMatch: query.count < 2) if !languageCode.lowercased().hasPrefix("en") { signal = signal |> mapToSignal { keywords in diff --git a/submodules/TelegramUI/TelegramUI/EmojisChatInputContextPanelNode.swift b/submodules/TelegramUI/TelegramUI/EmojisChatInputContextPanelNode.swift index 2ab319c268..c7efec6d06 100644 --- a/submodules/TelegramUI/TelegramUI/EmojisChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/TelegramUI/EmojisChatInputContextPanelNode.swift @@ -103,6 +103,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { private var enqueuedTransitions: [(EmojisChatInputContextPanelTransition, Bool)] = [] private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat)? + private var presentationInterfaceState: ChatPresentationInterfaceState? override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) { self.backgroundNode = ASImageNode() @@ -191,6 +192,10 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { }) self.currentEntries = to self.enqueueTransition(transition, firstTime: firstTime) + + if let presentationInterfaceState = presentationInterfaceState, let (size, leftInset, rightInset, bottomInset) = self.validLayout { + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, transition: .immediate, interfaceState: presentationInterfaceState) + } } private func enqueueTransition(_ transition: EmojisChatInputContextPanelTransition, firstTime: Bool) { @@ -208,12 +213,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() - if firstTime { - //options.insert(.Synchronous) - //options.insert(.LowLatency) - } else { - options.insert(.AnimateCrossfade) - } + options.insert(.Synchronous) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), duration: 0.0, curve: .Default(duration: nil)) @@ -224,6 +224,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { let hadValidLayout = self.validLayout != nil self.validLayout = (size, leftInset, rightInset, bottomInset) + self.presentationInterfaceState = interfaceState let sideInsets: CGFloat = 10.0 + leftInset let contentWidth = min(size.width - sideInsets - sideInsets, max(24.0, CGFloat(self.currentEntries?.count ?? 0) * 45.0))