import Foundation import Display import SwiftSignalKit import Postbox import TelegramCore private final class RecentSessionsControllerArguments { let account: Account let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void let removeSession: (Int64) -> Void let terminateOtherSessions: () -> Void let removeWebSession: (Int64) -> Void let terminateAllWebSessions: () -> Void init(account: Account, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void) { self.account = account self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions self.removeSession = removeSession self.terminateOtherSessions = terminateOtherSessions self.removeWebSession = removeWebSession self.terminateAllWebSessions = terminateAllWebSessions } } private enum RecentSessionsMode: Int { case sessions case websites } private enum RecentSessionsSection: Int32 { case currentSession case pendingSessions case otherSessions } private enum RecentSessionsEntryStableId: Hashable { case session(Int64) case index(Int32) var hashValue: Int { switch self { case let .session(hash): return hash.hashValue case let .index(index): return index.hashValue } } static func ==(lhs: RecentSessionsEntryStableId, rhs: RecentSessionsEntryStableId) -> Bool { switch lhs { case let .session(hash): if case .session(hash) = rhs { return true } else { return false } case let .index(index): if case .index(index) = rhs { return true } else { return false } } } } private enum RecentSessionsEntry: ItemListNodeEntry { case currentSessionHeader(PresentationTheme, String) case currentSession(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, RecentAccountSession) case terminateOtherSessions(PresentationTheme, String) case terminateAllWebSessions(PresentationTheme, String) case currentSessionInfo(PresentationTheme, String) case pendingSessionsHeader(PresentationTheme, String) case pendingSession(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) case pendingSessionsInfo(PresentationTheme, String) case otherSessionsHeader(PresentationTheme, String) case session(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) case website(index: Int32, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool) var section: ItemListSectionId { switch self { case .currentSessionHeader, .currentSession, .terminateOtherSessions, .terminateAllWebSessions, .currentSessionInfo: return RecentSessionsSection.currentSession.rawValue case .pendingSessionsHeader, .pendingSession, .pendingSessionsInfo: return RecentSessionsSection.pendingSessions.rawValue case .otherSessionsHeader, .session, .website: return RecentSessionsSection.otherSessions.rawValue } } var stableId: RecentSessionsEntryStableId { switch self { case .currentSessionHeader: return .index(0) case .currentSession: return .index(1) case .terminateOtherSessions: return .index(2) case .terminateAllWebSessions: return .index(3) case .currentSessionInfo: return .index(4) case .pendingSessionsHeader: return .index(5) case let .pendingSession(_, _, _, _, session, _, _, _): return .session(session.hash) case .pendingSessionsInfo: return .index(6) case .otherSessionsHeader: return .index(7) case let .session(_, _, _, _, session, _, _, _): return .session(session.hash) case let .website(_, _, _, _, website, _, _, _, _): return .session(website.hash) } } static func ==(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool { switch lhs { case let .currentSessionHeader(lhsTheme, lhsText): if case let .currentSessionHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .terminateOtherSessions(lhsTheme, lhsText): if case let .terminateOtherSessions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .terminateAllWebSessions(lhsTheme, lhsText): if case let .terminateAllWebSessions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .currentSessionInfo(lhsTheme, lhsText): if case let .currentSessionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .pendingSessionsHeader(lhsTheme, lhsText): if case let .pendingSessionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .pendingSession(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsSession, lhsEnabled, lhsEditing, lhsRevealed): if case let .pendingSession(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsSession, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { return true } else { return false } case let .pendingSessionsInfo(lhsTheme, lhsText): if case let .pendingSessionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .otherSessionsHeader(lhsTheme, lhsText): if case let .otherSessionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .currentSession(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsSession): if case let .currentSession(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsSession) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession { return true } else { return false } case let .session(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsSession, lhsEnabled, lhsEditing, lhsRevealed): if case let .session(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsSession, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsSession == rhsSession, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { return true } else { return false } case let .website(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsWebsite, lhsPeer, lhsEnabled, lhsEditing, lhsRevealed): if case let .website(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsWebsite, rhsPeer, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsWebsite == rhsWebsite, arePeersEqual(lhsPeer, rhsPeer), lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { return true } else { return false } } } static func <(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool { switch lhs.stableId { case let .index(lhsIndex): if case let .index(rhsIndex) = rhs.stableId { return lhsIndex <= rhsIndex } else { if case .pendingSession = rhs, lhsIndex > 5 { return false } else { return true } } case .session: switch lhs { case let .session(lhsIndex, _, _, _, _, _, _, _): if case let .session(rhsIndex, _, _, _, _, _, _, _) = rhs { return lhsIndex <= rhsIndex } else { return false } case let .pendingSession(lhsIndex, _, _, _, _, _, _, _): if case let .pendingSession(rhsIndex, _, _, _, _, _, _, _) = rhs { return lhsIndex <= rhsIndex } else if case .session = rhs { return true } else { if case let .index(rhsIndex) = rhs.stableId { return rhsIndex == 6 } else { return false } } case let .website(lhsIndex, _, _, _, _, _, _, _, _): if case let .website(rhsIndex, _, _, _, _, _, _, _, _) = rhs { return lhsIndex <= rhsIndex } else { return false } default: preconditionFailure() } } } func item(_ arguments: RecentSessionsControllerArguments) -> ListViewItem { switch self { case let .currentSessionHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .currentSession(theme, strings, dateTimeFormat, session): return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: true, editable: false, editing: false, revealed: false, sectionId: self.section, setSessionIdWithRevealedOptions: { _, _ in }, removeSession: { _ in }) case let .terminateOtherSessions(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.terminateOtherSessions() }) case let .terminateAllWebSessions(theme, text): return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.terminateAllWebSessions() }) case let .currentSessionInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .pendingSessionsHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .pendingSession(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in arguments.setSessionIdWithRevealedOptions(previousId, id) }, removeSession: { id in arguments.removeSession(id) }) case let .pendingSessionsInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .otherSessionsHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .session(_, theme, strings, dateTimeFormat, session, enabled, editing, revealed): return ItemListRecentSessionItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in arguments.setSessionIdWithRevealedOptions(previousId, id) }, removeSession: { id in arguments.removeSession(id) }) case let .website(_, theme, strings, dateTimeFormat, website, peer, enabled, editing, revealed): return ItemListWebsiteItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, website: website, peer: peer, enabled: enabled, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in arguments.setSessionIdWithRevealedOptions(previousId, id) }, removeSession: { id in arguments.removeWebSession(id) }) } } } private struct RecentSessionsControllerState: Equatable { let editing: Bool let sessionIdWithRevealedOptions: Int64? let removingSessionId: Int64? let terminatingOtherSessions: Bool init() { self.editing = false self.sessionIdWithRevealedOptions = nil self.removingSessionId = nil self.terminatingOtherSessions = false } init(editing: Bool, sessionIdWithRevealedOptions: Int64?, removingSessionId: Int64?, terminatingOtherSessions: Bool) { self.editing = editing self.sessionIdWithRevealedOptions = sessionIdWithRevealedOptions self.removingSessionId = removingSessionId self.terminatingOtherSessions = terminatingOtherSessions } static func ==(lhs: RecentSessionsControllerState, rhs: RecentSessionsControllerState) -> Bool { if lhs.editing != rhs.editing { return false } if lhs.sessionIdWithRevealedOptions != rhs.sessionIdWithRevealedOptions { return false } if lhs.removingSessionId != rhs.removingSessionId { return false } if lhs.terminatingOtherSessions != rhs.terminatingOtherSessions { return false } return true } func withUpdatedEditing(_ editing: Bool) -> RecentSessionsControllerState { return RecentSessionsControllerState(editing: editing, sessionIdWithRevealedOptions: self.sessionIdWithRevealedOptions, removingSessionId: self.removingSessionId, terminatingOtherSessions: self.terminatingOtherSessions) } func withUpdatedSessionIdWithRevealedOptions(_ sessionIdWithRevealedOptions: Int64?) -> RecentSessionsControllerState { return RecentSessionsControllerState(editing: self.editing, sessionIdWithRevealedOptions: sessionIdWithRevealedOptions, removingSessionId: self.removingSessionId, terminatingOtherSessions: self.terminatingOtherSessions) } func withUpdatedRemovingSessionId(_ removingSessionId: Int64?) -> RecentSessionsControllerState { return RecentSessionsControllerState(editing: self.editing, sessionIdWithRevealedOptions: self.sessionIdWithRevealedOptions, removingSessionId: removingSessionId, terminatingOtherSessions: self.terminatingOtherSessions) } func withUpdatedTerminatingOtherSessions(_ terminatingOtherSessions: Bool) -> RecentSessionsControllerState { return RecentSessionsControllerState(editing: self.editing, sessionIdWithRevealedOptions: self.sessionIdWithRevealedOptions, removingSessionId: self.removingSessionId, terminatingOtherSessions: terminatingOtherSessions) } } private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, sessions: [RecentAccountSession]?) -> [RecentSessionsEntry] { var entries: [RecentSessionsEntry] = [] if let sessions = sessions { var existingSessionIds = Set() entries.append(.currentSessionHeader(presentationData.theme, presentationData.strings.AuthSessions_CurrentSession)) if let index = sessions.index(where: { $0.hash == 0 }) { existingSessionIds.insert(sessions[index].hash) entries.append(.currentSession(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, sessions[index])) } if sessions.count > 1 { entries.append(.terminateOtherSessions(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessions)) entries.append(.currentSessionInfo(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessionsHelp)) let filteredPendingSessions: [RecentAccountSession] = sessions.filter({ $0.flags.contains(.passwordPending) }) if !filteredPendingSessions.isEmpty { entries.append(.pendingSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_IncompleteAttempts)) for i in 0 ..< filteredPendingSessions.count { if !existingSessionIds.contains(filteredPendingSessions[i].hash) { existingSessionIds.insert(filteredPendingSessions[i].hash) entries.append(.pendingSession(index: Int32(i), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, session: filteredPendingSessions[i], enabled: state.removingSessionId != filteredPendingSessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == filteredPendingSessions[i].hash)) } } entries.append(.pendingSessionsInfo(presentationData.theme, presentationData.strings.AuthSessions_IncompleteAttemptsInfo)) } entries.append(.otherSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_OtherSessions)) let filteredSessions: [RecentAccountSession] = sessions.sorted(by: { lhs, rhs in return lhs.activityDate > rhs.activityDate }) for i in 0 ..< filteredSessions.count { if !existingSessionIds.contains(filteredSessions[i].hash) { existingSessionIds.insert(filteredSessions[i].hash) entries.append(.session(index: Int32(i), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, session: filteredSessions[i], enabled: state.removingSessionId != filteredSessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == filteredSessions[i].hash)) } } } } return entries } private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, websites: [WebAuthorization]?, peers: [PeerId : Peer]?) -> [RecentSessionsEntry] { var entries: [RecentSessionsEntry] = [] if let websites = websites, let peers = peers { var existingSessionIds = Set() if websites.count > 0 { entries.append(.terminateAllWebSessions(presentationData.theme, presentationData.strings.AuthSessions_LogOutApplications)) entries.append(.currentSessionInfo(presentationData.theme, presentationData.strings.AuthSessions_LogOutApplicationsHelp)) entries.append(.otherSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_LoggedInWithTelegram)) let filteredWebsites: [WebAuthorization] = websites.sorted(by: { lhs, rhs in return lhs.dateActive > rhs.dateActive }) for i in 0 ..< filteredWebsites.count { let website = websites[i] if !existingSessionIds.contains(website.hash) { existingSessionIds.insert(website.hash) entries.append(.website(index: Int32(i), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, website: website, peer: peers[website.botId], enabled: state.removingSessionId != website.hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == website.hash)) } } } } return entries } public func recentSessionsController(account: Account) -> ViewController { let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: RecentSessionsControllerState()) let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? let actionsDisposable = DisposableSet() let removeSessionDisposable = MetaDisposable() actionsDisposable.add(removeSessionDisposable) let terminateOtherSessionsDisposable = MetaDisposable() actionsDisposable.add(terminateOtherSessionsDisposable) let mode = ValuePromise(.sessions) let sessionsPromise = Promise<[RecentAccountSession]?>(nil) let websitesPromise = Promise<([WebAuthorization], [PeerId : Peer])?>(nil) let arguments = RecentSessionsControllerArguments(account: account, setSessionIdWithRevealedOptions: { sessionId, fromSessionId in updateState { state in if (sessionId == nil && fromSessionId == state.sessionIdWithRevealedOptions) || (sessionId != nil && fromSessionId == nil) { return state.withUpdatedSessionIdWithRevealedOptions(sessionId) } else { return state } } }, removeSession: { sessionId in let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateSession, color: .destructive, action: { dismissAction() updateState { return $0.withUpdatedRemovingSessionId(sessionId) } let applySessions: Signal = sessionsPromise.get() |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue |> mapToSignal { sessions -> Signal in if let sessions = sessions { var updatedSessions = sessions for i in 0 ..< updatedSessions.count { if updatedSessions[i].hash == sessionId { updatedSessions.remove(at: i) break } } sessionsPromise.set(.single(updatedSessions)) } return .complete() } removeSessionDisposable.set((terminateAccountSession(account: account, hash: sessionId) |> then((applySessions |> mapError { _ in TerminateSessionError.generic })) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingSessionId(nil) } }, completed: { updateState { return $0.withUpdatedRemovingSessionId(nil) } })) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, terminateOtherSessions: { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.AuthSessions_TerminateOtherSessions, color: .destructive, action: { dismissAction() updateState { return $0.withUpdatedTerminatingOtherSessions(true) } let applySessions: Signal = sessionsPromise.get() |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue |> mapToSignal { sessions -> Signal in if let sessions = sessions { let updatedSessions = sessions.filter { $0.isCurrent } sessionsPromise.set(.single(updatedSessions)) } return .complete() } terminateOtherSessionsDisposable.set((terminateOtherAccountSessions(account: account) |> then(applySessions) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedTerminatingOtherSessions(false) } }, completed: { updateState { return $0.withUpdatedTerminatingOtherSessions(false) } })) }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, removeWebSession: { sessionId in updateState { 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: account.network, hash: sessionId) |> mapToSignal { _ -> Signal in return .complete() }) |> then(applySessions) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingSessionId(nil) } }, completed: { updateState { return $0.withUpdatedRemovingSessionId(nil) } })) }, terminateAllWebSessions: { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } let controller = ActionSheetController(presentationTheme: presentationData.theme) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.AuthSessions_LogOutApplications, color: .destructive, action: { dismissAction() updateState { return $0.withUpdatedTerminatingOtherSessions(true) } terminateOtherSessionsDisposable.set((terminateAllWebSessions(network: account.network) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedTerminatingOtherSessions(false) } }, completed: { updateState { 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)) }) let sessionsSignal: Signal<[RecentAccountSession]?, NoError> = .single(nil) |> then(requestRecentAccountSessions(account: account) |> map(Optional.init)) sessionsPromise.set(sessionsSignal) let websitesSignal: Signal<([WebAuthorization], [PeerId : Peer])?, NoError> = .single(nil) |> then(webSessions(network: account.network) |> map(Optional.init)) websitesPromise.set(websitesSignal) let previousMode = Atomic(value: .sessions) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, mode.get(), statePromise.get(), sessionsPromise.get(), websitesPromise.get()) |> deliverOnMainQueue |> map { presentationData, mode, state, sessions, websitesAndPeers -> (ItemListControllerState, (ItemListNodeState, RecentSessionsEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? let websites = websitesAndPeers?.0 let peers = websitesAndPeers?.1 if let sessions = sessions, sessions.count > 1 { if state.terminatingOtherSessions { rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {}) } else if state.editing { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } }) } } var emptyStateItem: ItemListControllerEmptyStateItem? if sessions == nil { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let title: ItemListControllerTitle let entries: [RecentSessionsEntry] if let websites = websites, !websites.isEmpty { title = .sectionControl([presentationData.strings.AuthSessions_Sessions, presentationData.strings.AuthSessions_LoggedIn], mode.rawValue) } else { title = .text(presentationData.strings.AuthSessions_Title) } var animateChanges = true switch (mode, websites, peers) { case (.websites, let websites, let peers): entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, websites: websites, peers: peers) default: entries = recentSessionsControllerEntries(presentationData: presentationData, state: state, sessions: sessions) } let previousMode = previousMode.swap(mode) var crossfadeState = false if previousMode != mode { crossfadeState = true animateChanges = false } let controllerState = ItemListControllerState(theme: presentationData.theme, title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let listState = ItemListNodeState(entries: entries, style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: crossfadeState, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(account: account, state: signal) controller.titleControlValueChanged = { [weak mode] index in mode?.set(index == 0 ? .sessions : .websites) } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) } } return controller }