Improve QR auth and sessions UI

This commit is contained in:
Ali 2019-12-09 19:35:43 +04:00
parent 84717460e2
commit 39ceca96aa
10 changed files with 366 additions and 79 deletions

View File

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

View File

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

View File

@ -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()
videoLayer.flushAndRemoveImage()
takeFrameQueue.after(1.0, {
videoLayer.flushAndRemoveImage()
})
}
Queue.mainQueue().after(1.0, {
videoLayer.flushAndRemoveImage()
})
}
}
}

View File

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

View File

@ -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<Void, NoError> = websitesPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { websitesAndPeers -> Signal<Void, NoError> 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<Void, NoError> in
return .complete()
}) |> then(applySessions) |> deliverOnMainQueue).start(error: { _ in
removeSessionDisposable.set(((webSessionsContext.remove(hash: sessionId)
|> mapToSignal { _ -> Signal<Void, NoError> 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<RecentSessionsMode>(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)

View File

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

View File

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

View File

@ -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<WebSessionsContextState>()
public var state: Signal<WebSessionsContextState, NoError> {
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<Never, NoError> {
assert(Queue.mainQueue().isCurrent())
return terminateWebSession(network: self.account.network, hash: hash)
|> deliverOnMainQueue
|> mapToSignal { [weak self] _ -> Signal<Never, NoError> 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<Never, NoError> {
return terminateAllWebSessions(network: self.account.network)
|> deliverOnMainQueue
|> mapToSignal { [weak self] _ -> Signal<Never, NoError> in
guard let strongSelf = self else {
return .complete()
}
strongSelf._state = WebSessionsContextState(isLoadingMore: strongSelf._state.isLoadingMore, sessions: [], peers: [:])
return .complete()
}
}
}

View File

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

View File

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