mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-28 10:55:40 +00:00
no message
This commit is contained in:
parent
be625d8f96
commit
c5d34b17f0
22
Images.xcassets/Chat/Message/ImpressionCount.imageset/Contents.json
vendored
Normal file
22
Images.xcassets/Chat/Message/ImpressionCount.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MessageInlineViewCountIconOutgoing@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "MessageInlineViewCountIconOutgoing@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Images.xcassets/Chat/Message/ImpressionCount.imageset/MessageInlineViewCountIconOutgoing@2x.png
vendored
Normal file
BIN
Images.xcassets/Chat/Message/ImpressionCount.imageset/MessageInlineViewCountIconOutgoing@2x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 397 B |
BIN
Images.xcassets/Chat/Message/ImpressionCount.imageset/MessageInlineViewCountIconOutgoing@3x.png
vendored
Normal file
BIN
Images.xcassets/Chat/Message/ImpressionCount.imageset/MessageInlineViewCountIconOutgoing@3x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 777 B |
@ -192,6 +192,11 @@
|
||||
D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */; };
|
||||
D0568AAF1DF1B3920022E7DA /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */; };
|
||||
D05811941DD5F9380057C769 /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */; };
|
||||
D05A32DC1E6EFCC2002760B4 /* NumericFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A32DB1E6EFCC2002760B4 /* NumericFormat.swift */; };
|
||||
D05A32DE1E6F0097002760B4 /* PrivacyAndSecurityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A32DD1E6F0097002760B4 /* PrivacyAndSecurityController.swift */; };
|
||||
D05A32EA1E6F143C002760B4 /* RecentSessionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A32E91E6F143C002760B4 /* RecentSessionsController.swift */; };
|
||||
D05A32EC1E6F1462002760B4 /* BlockedPeersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A32EB1E6F1462002760B4 /* BlockedPeersController.swift */; };
|
||||
D05A32EE1E6F25A0002760B4 /* ItemListRecentSessionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A32ED1E6F25A0002760B4 /* ItemListRecentSessionItem.swift */; };
|
||||
D0613FC81E5F8AB100202CDB /* ChannelInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */; };
|
||||
D0613FCD1E60482300202CDB /* ChannelMembersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0613FCC1E60482300202CDB /* ChannelMembersController.swift */; };
|
||||
D0613FD51E6064D200202CDB /* ConvertToSupergroupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */; };
|
||||
@ -670,6 +675,11 @@
|
||||
D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.swift; sourceTree = "<group>"; };
|
||||
D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = "<group>"; };
|
||||
D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = "<group>"; };
|
||||
D05A32DB1E6EFCC2002760B4 /* NumericFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumericFormat.swift; sourceTree = "<group>"; };
|
||||
D05A32DD1E6F0097002760B4 /* PrivacyAndSecurityController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyAndSecurityController.swift; sourceTree = "<group>"; };
|
||||
D05A32E91E6F143C002760B4 /* RecentSessionsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentSessionsController.swift; sourceTree = "<group>"; };
|
||||
D05A32EB1E6F1462002760B4 /* BlockedPeersController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockedPeersController.swift; sourceTree = "<group>"; };
|
||||
D05A32ED1E6F25A0002760B4 /* ItemListRecentSessionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListRecentSessionItem.swift; sourceTree = "<group>"; };
|
||||
D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoController.swift; sourceTree = "<group>"; };
|
||||
D0613FCC1E60482300202CDB /* ChannelMembersController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMembersController.swift; sourceTree = "<group>"; };
|
||||
D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertToSupergroupController.swift; sourceTree = "<group>"; };
|
||||
@ -1531,6 +1541,10 @@
|
||||
D0CE1BD21E51BC6100404327 /* DebugController.swift */,
|
||||
D03E5E081E55C49C0029569A /* DebugAccountsController.swift */,
|
||||
D0528E671E65CB2C00E2FEF5 /* UsernameSetupController.swift */,
|
||||
D05A32DD1E6F0097002760B4 /* PrivacyAndSecurityController.swift */,
|
||||
D05A32ED1E6F25A0002760B4 /* ItemListRecentSessionItem.swift */,
|
||||
D05A32E91E6F143C002760B4 /* RecentSessionsController.swift */,
|
||||
D05A32EB1E6F1462002760B4 /* BlockedPeersController.swift */,
|
||||
);
|
||||
name = Settings;
|
||||
sourceTree = "<group>";
|
||||
@ -1775,8 +1789,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */,
|
||||
D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */,
|
||||
D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */,
|
||||
D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */,
|
||||
D0486F091E523C8500091F0C /* GroupInfoController.swift */,
|
||||
D03E5E0E1E55F8B90029569A /* ChannelVisibilityController.swift */,
|
||||
D0561DE71E574C3200E6B9E9 /* ChannelAdminsController.swift */,
|
||||
@ -2179,6 +2193,7 @@
|
||||
D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */,
|
||||
D0DA44551E4E7F43005FDCA7 /* ShakeAnimation.swift */,
|
||||
D0E305A41E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift */,
|
||||
D05A32DB1E6EFCC2002760B4 /* NumericFormat.swift */,
|
||||
);
|
||||
name = Utils;
|
||||
sourceTree = "<group>";
|
||||
@ -2445,6 +2460,7 @@
|
||||
D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */,
|
||||
D0BC38811E40F1D80044D6FE /* ContactSelectionControllerNode.swift in Sources */,
|
||||
D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */,
|
||||
D05A32EE1E6F25A0002760B4 /* ItemListRecentSessionItem.swift in Sources */,
|
||||
D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */,
|
||||
D07827C71E01CD5900071108 /* VerticalListContextResultsChatInputPanelButtonItem.swift in Sources */,
|
||||
D021E0E51DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift in Sources */,
|
||||
@ -2708,6 +2724,7 @@
|
||||
D0E7A1BF1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift in Sources */,
|
||||
D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */,
|
||||
D07CFF761DCA224100761F81 /* PeerSelectionControllerNode.swift in Sources */,
|
||||
D05A32EA1E6F143C002760B4 /* RecentSessionsController.swift in Sources */,
|
||||
D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */,
|
||||
D00C7CDE1E37770A0080C3D5 /* SecretMediaPreviewControllerNode.swift in Sources */,
|
||||
D01B27951E38F3BF0022A4C0 /* ItemListControllerNode.swift in Sources */,
|
||||
@ -2717,6 +2734,7 @@
|
||||
D050F2161E48D9E000988324 /* AuthorizationSequenceCountrySelectionController.swift in Sources */,
|
||||
D0561DDF1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift in Sources */,
|
||||
D01749551E1082770057C89A /* StoredMessageFromSearchPeer.swift in Sources */,
|
||||
D05A32DC1E6EFCC2002760B4 /* NumericFormat.swift in Sources */,
|
||||
D04BB32C1E48797500650E93 /* animations.c in Sources */,
|
||||
D049EAEE1E44BB3200A2CD3A /* ChatListRecentPeersListItem.swift in Sources */,
|
||||
D04BB2BB1E44EA2400650E93 /* AuthorizationSequenceSplashControllerNode.swift in Sources */,
|
||||
@ -2747,6 +2765,7 @@
|
||||
D0DE77231D932043002B8809 /* PeerMediaCollectionInterfaceState.swift in Sources */,
|
||||
D0F69D781D6B87DF0046BCD6 /* MediaTrackFrameBuffer.swift in Sources */,
|
||||
D01AC91F1DD5E09000E8160F /* EditAccessoryPanelNode.swift in Sources */,
|
||||
D05A32EC1E6F1462002760B4 /* BlockedPeersController.swift in Sources */,
|
||||
D023EBB21DDA800700BD496D /* LegacyMediaPickers.swift in Sources */,
|
||||
D0F69DD01D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift in Sources */,
|
||||
D03120F61DA534C1006A2A60 /* ItemListActionItem.swift in Sources */,
|
||||
@ -2782,6 +2801,7 @@
|
||||
D0215D3C1E041014001A0B1E /* InstantPageItem.swift in Sources */,
|
||||
D0E35A071DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift in Sources */,
|
||||
D0D03AE51DECAE8900220C46 /* ManagedAudioRecorder.swift in Sources */,
|
||||
D05A32DE1E6F0097002760B4 /* PrivacyAndSecurityController.swift in Sources */,
|
||||
D0EE971A1D88BCA0006C18E1 /* ChatInfo.swift in Sources */,
|
||||
D0F69DE31D6B8A420046BCD6 /* ListControllerItem.swift in Sources */,
|
||||
D0736F211DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift in Sources */,
|
||||
|
||||
279
TelegramUI/BlockedPeersController.swift
Normal file
279
TelegramUI/BlockedPeersController.swift
Normal file
@ -0,0 +1,279 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
private final class BlockedPeersControllerArguments {
|
||||
let account: Account
|
||||
|
||||
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
||||
let removePeer: (PeerId) -> Void
|
||||
|
||||
init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void) {
|
||||
self.account = account
|
||||
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
||||
self.removePeer = removePeer
|
||||
}
|
||||
}
|
||||
|
||||
private enum BlockedPeersSection: Int32 {
|
||||
case peers
|
||||
}
|
||||
|
||||
private enum BlockedPeersEntryStableId: Hashable {
|
||||
case peer(PeerId)
|
||||
|
||||
var hashValue: Int {
|
||||
switch self {
|
||||
case let .peer(peerId):
|
||||
return peerId.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: BlockedPeersEntryStableId, rhs: BlockedPeersEntryStableId) -> Bool {
|
||||
switch lhs {
|
||||
case let .peer(peerId):
|
||||
if case .peer(peerId) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum BlockedPeersEntry: ItemListNodeEntry {
|
||||
case peerItem(Int32, Peer, ItemListPeerItemEditing, Bool)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .peerItem:
|
||||
return BlockedPeersSection.peers.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: BlockedPeersEntryStableId {
|
||||
switch self {
|
||||
case let .peerItem(_, peer, _, _):
|
||||
return .peer(peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .peerItem(lhsIndex, lhsPeer, lhsEditing, lhsEnabled):
|
||||
if case let .peerItem(rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
if lhsEditing != rhsEditing {
|
||||
return false
|
||||
}
|
||||
if lhsEnabled != rhsEnabled {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .peerItem(index, _, _, _):
|
||||
switch rhs {
|
||||
case let .peerItem(rhsIndex, _, _, _):
|
||||
return index < rhsIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(_ arguments: BlockedPeersControllerArguments) -> ListViewItem {
|
||||
switch self {
|
||||
case let .peerItem(_, peer, editing, enabled):
|
||||
return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .none, label: nil, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
|
||||
arguments.setPeerIdWithRevealedOptions(previousId, id)
|
||||
}, removePeer: { peerId in
|
||||
arguments.removePeer(peerId)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct BlockedPeersControllerState: Equatable {
|
||||
let editing: Bool
|
||||
let peerIdWithRevealedOptions: PeerId?
|
||||
let removingPeerId: PeerId?
|
||||
|
||||
init() {
|
||||
self.editing = false
|
||||
self.peerIdWithRevealedOptions = nil
|
||||
self.removingPeerId = nil
|
||||
}
|
||||
|
||||
init(editing: Bool, peerIdWithRevealedOptions: PeerId?, removingPeerId: PeerId?) {
|
||||
self.editing = editing
|
||||
self.peerIdWithRevealedOptions = peerIdWithRevealedOptions
|
||||
self.removingPeerId = removingPeerId
|
||||
}
|
||||
|
||||
static func ==(lhs: BlockedPeersControllerState, rhs: BlockedPeersControllerState) -> Bool {
|
||||
if lhs.editing != rhs.editing {
|
||||
return false
|
||||
}
|
||||
if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions {
|
||||
return false
|
||||
}
|
||||
if lhs.removingPeerId != rhs.removingPeerId {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func withUpdatedEditing(_ editing: Bool) -> BlockedPeersControllerState {
|
||||
return BlockedPeersControllerState(editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId)
|
||||
}
|
||||
|
||||
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> BlockedPeersControllerState {
|
||||
return BlockedPeersControllerState(editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, removingPeerId: self.removingPeerId)
|
||||
}
|
||||
|
||||
func withUpdatedRemovingPeerId(_ removingPeerId: PeerId?) -> BlockedPeersControllerState {
|
||||
return BlockedPeersControllerState(editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: removingPeerId)
|
||||
}
|
||||
}
|
||||
|
||||
private func blockedPeersControllerEntries(state: BlockedPeersControllerState, peers: [Peer]?) -> [BlockedPeersEntry] {
|
||||
var entries: [BlockedPeersEntry] = []
|
||||
|
||||
if let peers = peers {
|
||||
var index: Int32 = 0
|
||||
for peer in peers {
|
||||
entries.append(.peerItem(index, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != peer.id))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func blockedPeersController(account: Account) -> ViewController {
|
||||
let statePromise = ValuePromise(BlockedPeersControllerState(), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: BlockedPeersControllerState())
|
||||
let updateState: ((BlockedPeersControllerState) -> BlockedPeersControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let removePeerDisposable = MetaDisposable()
|
||||
actionsDisposable.add(removePeerDisposable)
|
||||
|
||||
let peersPromise = Promise<[Peer]?>(nil)
|
||||
|
||||
let arguments = BlockedPeersControllerArguments(account: account, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
||||
updateState { state in
|
||||
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
|
||||
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}, removePeer: { memberId in
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingPeerId(memberId)
|
||||
}
|
||||
|
||||
let applyPeers: Signal<Void, NoError> = peersPromise.get()
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { peers -> Signal<Void, NoError> in
|
||||
if let peers = peers {
|
||||
var updatedPeers = peers
|
||||
for i in 0 ..< updatedPeers.count {
|
||||
if updatedPeers[i].id == memberId {
|
||||
updatedPeers.remove(at: i)
|
||||
break
|
||||
}
|
||||
}
|
||||
peersPromise.set(.single(updatedPeers))
|
||||
}
|
||||
|
||||
return .complete()
|
||||
}
|
||||
|
||||
removePeerDisposable.set((requestUpdatePeerIsBlocked(account: account, peerId: memberId, isBlocked: false) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingPeerId(nil)
|
||||
}
|
||||
}, completed: {
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingPeerId(nil)
|
||||
}
|
||||
|
||||
}))
|
||||
})
|
||||
|
||||
let peersSignal: Signal<[Peer]?, NoError> = .single(nil) |> then(requestBlockedPeers(account: account) |> map { Optional($0) })
|
||||
|
||||
peersPromise.set(peersSignal)
|
||||
|
||||
var previousPeers: [Peer]?
|
||||
|
||||
let signal = combineLatest(statePromise.get(), peersPromise.get())
|
||||
|> deliverOnMainQueue
|
||||
|> map { state, peers -> (ItemListControllerState, (ItemListNodeState<BlockedPeersEntry>, BlockedPeersEntry.ItemGenerationArguments)) in
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
if let peers = peers, !peers.isEmpty {
|
||||
if state.editing {
|
||||
rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: {
|
||||
updateState { state in
|
||||
return state.withUpdatedEditing(false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
|
||||
updateState { state in
|
||||
return state.withUpdatedEditing(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
if let peers = peers {
|
||||
if peers.isEmpty {
|
||||
emptyStateItem = ItemListTextEmptyStateItem(text: "Blocked users can't send you messages of add you to groups. They will not see your profile pictures, online and last seen status.")
|
||||
}
|
||||
} else if peers == nil {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem()
|
||||
}
|
||||
|
||||
let previous = previousPeers
|
||||
previousPeers = peers
|
||||
|
||||
let controllerState = ItemListControllerState(title: "Blocked Users", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
|
||||
let listState = ItemListNodeState(entries: blockedPeersControllerEntries(state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(signal)
|
||||
presentControllerImpl = { [weak controller] c, p in
|
||||
if let controller = controller {
|
||||
controller.present(c, in: .window, with: p)
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
@ -477,7 +477,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
var timeinfo = tm()
|
||||
localtime_r(&t, &timeinfo)
|
||||
|
||||
let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
let dateText = stringForRelativeTimestamp(item.index.messageIndex.timestamp, relativeTo: timestamp)
|
||||
|
||||
dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: UIColor(0x8e8e93))
|
||||
|
||||
|
||||
@ -76,15 +76,15 @@ enum ChatListRecentEntry: Comparable, Identifiable {
|
||||
|
||||
static func <(lhs: ChatListRecentEntry, rhs: ChatListRecentEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .topPeers:
|
||||
return true
|
||||
case let .peer(lhsIndex, _):
|
||||
switch rhs {
|
||||
case .topPeers:
|
||||
return false
|
||||
case let .peer(rhsIndex, _):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
case .topPeers:
|
||||
return true
|
||||
case let .peer(lhsIndex, _):
|
||||
switch rhs {
|
||||
case .topPeers:
|
||||
return false
|
||||
case let .peer(rhsIndex, _):
|
||||
return lhsIndex <= rhsIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,7 +193,7 @@ enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
switch lhs {
|
||||
case let .localPeer(_, lhsIndex):
|
||||
if case let .localPeer(_, rhsIndex) = rhs {
|
||||
return lhsIndex < rhsIndex
|
||||
return lhsIndex <= rhsIndex
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
@ -202,7 +202,7 @@ enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
case .localPeer:
|
||||
return false
|
||||
case let .globalPeer(_, rhsIndex):
|
||||
return lhsIndex < rhsIndex
|
||||
return lhsIndex <= rhsIndex
|
||||
case .message:
|
||||
return true
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
if lhsIndex == rhsIndex {
|
||||
return lhsInfo.id.id < rhsInfo.id.id
|
||||
} else {
|
||||
return lhsIndex < rhsIndex
|
||||
return lhsIndex <= rhsIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,11 @@ import Foundation
|
||||
import Display
|
||||
|
||||
private let incomingFillColor = UIColor(0xffffff)
|
||||
private let incomingFillHighlightedColor = UIColor(0xaaaaff)
|
||||
private let incomingFillHighlightedColor = UIColor(0xd9f4ff)
|
||||
private let incomingStrokeColor = UIColor(0x86A9C9, 0.5)
|
||||
|
||||
private let outgoingFillColor = UIColor(0xE1FFC7)
|
||||
private let outgoingFillHighlightedColor = UIColor(0xaaffaa)
|
||||
private let outgoingFillHighlightedColor = UIColor(0xc8ffa6)
|
||||
private let outgoingStrokeColor = UIColor(0x86A9C9, 0.5)
|
||||
|
||||
enum MessageBubbleImageNeighbors {
|
||||
|
||||
@ -77,6 +77,10 @@ private let clockBubbleMinImage = generateClockMinImage(color: UIColor(0x42b649)
|
||||
private let clockMediaFrameImage = generateClockFrameImage(color: .white)
|
||||
private let clockMediaMinImage = generateClockMinImage(color: .white)
|
||||
|
||||
private let incomingImpressionIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ImpressionCount"), color: incomingDateColor)
|
||||
private let outgoingImpressionIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ImpressionCount"), color: outgoingDateColor)
|
||||
private let mediaImpressionIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ImpressionCount"), color: .white)
|
||||
|
||||
enum ChatMessageDateAndStatusOutgoingType {
|
||||
case Sent(read: Bool)
|
||||
case Sending
|
||||
@ -97,6 +101,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
private var clockFrameNode: ASImageNode?
|
||||
private var clockMinNode: ASImageNode?
|
||||
private let dateNode: TextNode
|
||||
private var impressionIcon: ASImageNode?
|
||||
|
||||
override init() {
|
||||
self.dateNode = TextNode()
|
||||
@ -108,7 +113,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
self.addSubnode(self.dateNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> Void) {
|
||||
func asyncLayout() -> (_ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> Void) {
|
||||
let dateLayout = TextNode.asyncLayout(self.dateNode)
|
||||
|
||||
var checkReadNode = self.checkReadNode
|
||||
@ -117,8 +122,9 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
var clockMinNode = self.clockMinNode
|
||||
|
||||
var currentBackgroundNode = self.backgroundNode
|
||||
var currentImpressionIcon = self.impressionIcon
|
||||
|
||||
return { dateText, type, constrainedSize in
|
||||
return { edited, impressionCount, dateText, type, constrainedSize in
|
||||
let dateColor: UIColor
|
||||
var backgroundImage: UIImage?
|
||||
var outgoingStatus: ChatMessageDateAndStatusOutgoingType?
|
||||
@ -128,6 +134,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
let loadedCheckPartialImage: UIImage?
|
||||
let clockFrameImage: UIImage?
|
||||
let clockMinImage: UIImage?
|
||||
var impressionImage: UIImage?
|
||||
|
||||
switch type {
|
||||
case .BubbleIncoming:
|
||||
@ -137,6 +144,9 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
loadedCheckPartialImage = checkBubblePartialImage
|
||||
clockFrameImage = clockBubbleFrameImage
|
||||
clockMinImage = clockBubbleMinImage
|
||||
if impressionCount != nil {
|
||||
impressionImage = incomingImpressionIcon
|
||||
}
|
||||
case let .BubbleOutgoing(status):
|
||||
dateColor = outgoingDateColor
|
||||
outgoingStatus = status
|
||||
@ -145,6 +155,9 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
loadedCheckPartialImage = checkBubblePartialImage
|
||||
clockFrameImage = clockBubbleFrameImage
|
||||
clockMinImage = clockBubbleMinImage
|
||||
if impressionCount != nil {
|
||||
impressionImage = outgoingImpressionIcon
|
||||
}
|
||||
case .ImageIncoming:
|
||||
dateColor = .white
|
||||
backgroundImage = imageBackground
|
||||
@ -153,6 +166,9 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
loadedCheckPartialImage = checkMediaPartialImage
|
||||
clockFrameImage = clockMediaFrameImage
|
||||
clockMinImage = clockMediaMinImage
|
||||
if impressionCount != nil {
|
||||
impressionImage = mediaImpressionIcon
|
||||
}
|
||||
case let .ImageOutgoing(status):
|
||||
dateColor = .white
|
||||
outgoingStatus = status
|
||||
@ -162,9 +178,21 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
loadedCheckPartialImage = checkMediaPartialImage
|
||||
clockFrameImage = clockMediaFrameImage
|
||||
clockMinImage = clockMediaMinImage
|
||||
if impressionCount != nil {
|
||||
impressionImage = mediaImpressionIcon
|
||||
}
|
||||
}
|
||||
|
||||
let (date, dateApply) = dateLayout(NSAttributedString(string: dateText, font: dateFont, textColor: dateColor), nil, 1, .end, constrainedSize, nil)
|
||||
var updatedDateText = dateText
|
||||
if let impressionCount = impressionCount {
|
||||
updatedDateText = compactNumericCountString(impressionCount) + " " + updatedDateText
|
||||
}
|
||||
|
||||
if edited {
|
||||
updatedDateText = "edited " + updatedDateText
|
||||
}
|
||||
|
||||
let (date, dateApply) = dateLayout(NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), nil, 1, .end, constrainedSize, nil)
|
||||
|
||||
let statusWidth: CGFloat
|
||||
|
||||
@ -267,7 +295,23 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
backgroundInsets = UIEdgeInsets(top: 2.0, left: 7.0, bottom: 2.0, right: 7.0)
|
||||
}
|
||||
|
||||
let layoutSize = CGSize(width: leftInset + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
|
||||
var impressionSize = CGSize()
|
||||
var impressionWidth: CGFloat = 0.0
|
||||
if let impressionImage = impressionImage {
|
||||
if currentImpressionIcon == nil {
|
||||
let iconNode = ASImageNode()
|
||||
iconNode.isLayerBacked = true
|
||||
iconNode.displayWithoutProcessing = true
|
||||
iconNode.displaysAsynchronously = false
|
||||
currentImpressionIcon = iconNode
|
||||
}
|
||||
impressionSize = impressionImage.size
|
||||
impressionWidth = impressionSize.width + 3.0
|
||||
} else {
|
||||
currentImpressionIcon = nil
|
||||
}
|
||||
|
||||
let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
|
||||
|
||||
return (layoutSize, { [weak self] animated in
|
||||
if let strongSelf = self {
|
||||
@ -288,7 +332,21 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode {
|
||||
|
||||
let _ = dateApply()
|
||||
|
||||
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left, y: backgroundInsets.top), size: date.size)
|
||||
if let currentImpressionIcon = currentImpressionIcon {
|
||||
if currentImpressionIcon.image !== impressionImage {
|
||||
currentImpressionIcon.image = impressionImage
|
||||
}
|
||||
if currentImpressionIcon.supernode == nil {
|
||||
strongSelf.impressionIcon = currentImpressionIcon
|
||||
strongSelf.addSubnode(currentImpressionIcon)
|
||||
}
|
||||
currentImpressionIcon.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left, y: backgroundInsets.top + 3.0), size: impressionSize)
|
||||
} else if let impressionIcon = strongSelf.impressionIcon {
|
||||
impressionIcon.removeFromSupernode()
|
||||
strongSelf.impressionIcon = nil
|
||||
}
|
||||
|
||||
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top), size: date.size)
|
||||
|
||||
if let clockFrameNode = clockFrameNode {
|
||||
if strongSelf.clockFrameNode == nil {
|
||||
|
||||
@ -162,21 +162,15 @@ final class ChatMessageInteractiveFileNode: ASTransformNode {
|
||||
var edited = false
|
||||
var viewCount: Int?
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? EditedMessageAttribute {
|
||||
if let _ = attribute as? EditedMessageAttribute {
|
||||
edited = true
|
||||
} else if let attribute = attribute as? ViewCountMessageAttribute {
|
||||
viewCount = attribute.count
|
||||
}
|
||||
}
|
||||
var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
|
||||
if let viewCount = viewCount {
|
||||
dateText = "\(viewCount) " + dateText
|
||||
}
|
||||
if edited {
|
||||
dateText = "edited " + dateText
|
||||
}
|
||||
let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
|
||||
|
||||
let (size, apply) = statusLayout(dateText, statusType, constrainedSize)
|
||||
let (size, apply) = statusLayout(edited, viewCount, dateText, statusType, constrainedSize)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
|
||||
@ -75,12 +75,6 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
|
||||
if let viewCount = viewCount {
|
||||
dateText = "\(viewCount) " + dateText
|
||||
}
|
||||
if edited {
|
||||
dateText = "edited " + dateText
|
||||
}
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
if case .None = position.bottom {
|
||||
@ -105,7 +99,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
let (size, apply) = statusLayout(dateText, statusType, CGSize(width: imageLayoutSize.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
let (size, apply) = statusLayout(edited, viewCount, dateText, statusType, CGSize(width: imageLayoutSize.width, height: CGFloat.greatestFiniteMagnitude))
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
|
||||
@ -56,14 +56,6 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
|
||||
if let viewCount = viewCount {
|
||||
dateText = "\(viewCount) " + dateText
|
||||
}
|
||||
if edited {
|
||||
dateText = "edited " + dateText
|
||||
}
|
||||
|
||||
//let dateText = "\(message.id.id)"
|
||||
|
||||
let statusType: ChatMessageDateAndStatusType?
|
||||
if case .None = position.bottom {
|
||||
@ -86,7 +78,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var statusApply: ((Bool) -> Void)?
|
||||
|
||||
if let statusType = statusType {
|
||||
let (size, apply) = statusLayout(dateText, statusType, textConstrainedSize)
|
||||
let (size, apply) = statusLayout(edited, viewCount, dateText, statusType, textConstrainedSize)
|
||||
statusSize = size
|
||||
statusApply = apply
|
||||
}
|
||||
@ -195,7 +187,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.addSubnode(strongSelf.statusNode)
|
||||
} else {
|
||||
if case let .System(duration) = animation {
|
||||
let delta = CGPoint(x: previousStatusFrame.minX - adjustedStatusFrame.minX, y: previousStatusFrame.minY - adjustedStatusFrame.minY)
|
||||
let delta = CGPoint(x: previousStatusFrame.maxX - adjustedStatusFrame.maxX, y: previousStatusFrame.minY - adjustedStatusFrame.minY)
|
||||
let statusPosition = strongSelf.statusNode.layer.position
|
||||
let previousPosition = CGPoint(x: statusPosition.x + delta.x, y: statusPosition.y + delta.y)
|
||||
strongSelf.statusNode.layer.animatePosition(from: previousPosition, to: statusPosition, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
@ -96,19 +96,13 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var edited = false
|
||||
var viewCount: Int?
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? EditedMessageAttribute {
|
||||
if let _ = attribute as? EditedMessageAttribute {
|
||||
edited = true
|
||||
} else if let attribute = attribute as? ViewCountMessageAttribute {
|
||||
viewCount = attribute.count
|
||||
}
|
||||
}
|
||||
var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
|
||||
if let viewCount = viewCount {
|
||||
dateText = "\(viewCount) " + dateText
|
||||
}
|
||||
if edited {
|
||||
dateText = "edited " + dateText
|
||||
}
|
||||
let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)])
|
||||
|
||||
var textString: NSAttributedString?
|
||||
var inlineImageDimensions: CGSize?
|
||||
@ -188,7 +182,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
} else if item.message.flags.isSending {
|
||||
statusType = .BubbleOutgoing(.Sending)
|
||||
} else {
|
||||
statusType = .BubbleOutgoing(.Sent(read: true))
|
||||
statusType = .BubbleOutgoing(.Sent(read: item.read))
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,7 +191,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var statusSizeAndApply: (CGSize, (Bool) -> Void)?
|
||||
|
||||
if refineContentImageLayout == nil && refineContentFileLayout == nil {
|
||||
statusSizeAndApply = statusLayout(dateText, statusType, textConstrainedSize)
|
||||
statusSizeAndApply = statusLayout(edited, viewCount, dateText, statusType, textConstrainedSize)
|
||||
}
|
||||
|
||||
let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, textCutout)
|
||||
|
||||
@ -143,14 +143,14 @@ final class ChatTitleView: UIView {
|
||||
}
|
||||
if onlineCount > 1 {
|
||||
let string = NSMutableAttributedString()
|
||||
string.append(NSAttributedString(string: "\(group.participantCount) members, ", font: Font.regular(13.0), textColor: UIColor(0x787878)))
|
||||
string.append(NSAttributedString(string: "\(compactNumericCountString(group.participantCount)) members, ", font: Font.regular(13.0), textColor: UIColor(0x787878)))
|
||||
string.append(NSAttributedString(string: "\(onlineCount) online", font: Font.regular(13.0), textColor: UIColor(0x007ee5)))
|
||||
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
|
||||
self.infoNode.attributedText = string
|
||||
shouldUpdateLayout = true
|
||||
}
|
||||
} else {
|
||||
let string = NSAttributedString(string: "\(group.participantCount) members", font: Font.regular(13.0), textColor: UIColor(0x787878))
|
||||
let string = NSAttributedString(string: "\(compactNumericCountString(group.participantCount)) members", font: Font.regular(13.0), textColor: UIColor(0x787878))
|
||||
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
|
||||
self.infoNode.attributedText = string
|
||||
shouldUpdateLayout = true
|
||||
@ -158,7 +158,7 @@ final class ChatTitleView: UIView {
|
||||
}
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
|
||||
let string = NSAttributedString(string: "\(memberCount) members", font: Font.regular(13.0), textColor: UIColor(0x787878))
|
||||
let string = NSAttributedString(string: "\(compactNumericCountString(Int(memberCount))) members", font: Font.regular(13.0), textColor: UIColor(0x787878))
|
||||
if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) {
|
||||
self.infoNode.attributedText = string
|
||||
shouldUpdateLayout = true
|
||||
|
||||
400
TelegramUI/ItemListRecentSessionItem.swift
Normal file
400
TelegramUI/ItemListRecentSessionItem.swift
Normal file
@ -0,0 +1,400 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
struct ItemListRecentSessionItemEditing: Equatable {
|
||||
let editable: Bool
|
||||
let editing: Bool
|
||||
let revealed: Bool
|
||||
|
||||
static func ==(lhs: ItemListRecentSessionItemEditing, rhs: ItemListRecentSessionItemEditing) -> Bool {
|
||||
if lhs.editable != rhs.editable {
|
||||
return false
|
||||
}
|
||||
if lhs.editing != rhs.editing {
|
||||
return false
|
||||
}
|
||||
if lhs.revealed != rhs.revealed {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
enum ItemListRecentSessionItemText {
|
||||
case presence
|
||||
case text(String)
|
||||
case none
|
||||
}
|
||||
|
||||
final class ItemListRecentSessionItem: ListViewItem, ItemListItem {
|
||||
let session: RecentAccountSession
|
||||
let enabled: Bool
|
||||
let editable: Bool
|
||||
let editing: Bool
|
||||
let revealed: Bool
|
||||
let sectionId: ItemListSectionId
|
||||
let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void
|
||||
let removeSession: (Int64) -> Void
|
||||
|
||||
init(session: RecentAccountSession, enabled: Bool, editable: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void) {
|
||||
self.session = session
|
||||
self.enabled = enabled
|
||||
self.editable = editable
|
||||
self.editing = editing
|
||||
self.revealed = revealed
|
||||
self.sectionId = sectionId
|
||||
self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions
|
||||
self.removeSession = removeSession
|
||||
}
|
||||
|
||||
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ItemListRecentSessionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
completion(node, {
|
||||
return (nil, { apply(false) })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
|
||||
if let node = node as? ItemListRecentSessionItemNode {
|
||||
Queue.mainQueue().async {
|
||||
let makeLayout = node.asyncLayout()
|
||||
|
||||
var animated = true
|
||||
if case .None = animation {
|
||||
animated = false
|
||||
}
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, {
|
||||
apply(animated)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.medium(15.0)
|
||||
private let textFont = Font.regular(13.0)
|
||||
|
||||
class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private var disabledOverlayNode: ASDisplayNode?
|
||||
|
||||
private let titleNode: TextNode
|
||||
private let appNode: TextNode
|
||||
private let locationNode: TextNode
|
||||
private let labelNode: TextNode
|
||||
|
||||
private var layoutParams: (ItemListRecentSessionItem, CGFloat, ItemListNeighbors)?
|
||||
|
||||
private var editableControlNode: ItemListEditableControlNode?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc)
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isLayerBacked = true
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.appNode = TextNode()
|
||||
self.appNode.isLayerBacked = true
|
||||
self.appNode.contentMode = .left
|
||||
self.appNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.locationNode = TextNode()
|
||||
self.locationNode.isLayerBacked = true
|
||||
self.locationNode.contentMode = .left
|
||||
self.locationNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.isLayerBacked = true
|
||||
self.labelNode.contentMode = .left
|
||||
self.labelNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false)
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.appNode)
|
||||
self.addSubnode(self.locationNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListRecentSessionItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeAppLayout = TextNode.asyncLayout(self.appNode)
|
||||
let makeLocationLayout = TextNode.asyncLayout(self.locationNode)
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode)
|
||||
|
||||
var currentDisabledOverlayNode = self.disabledOverlayNode
|
||||
|
||||
return { item, width, neighbors in
|
||||
var titleAttributedString: NSAttributedString?
|
||||
var appAttributedString: NSAttributedString?
|
||||
var locationAttributedString: NSAttributedString?
|
||||
var labelAttributedString: NSAttributedString?
|
||||
|
||||
let peerRevealOptions: [ItemListRevealOption]
|
||||
if item.editable && item.enabled {
|
||||
peerRevealOptions = [ItemListRevealOption(key: 0, title: "Terminate", icon: nil, color: UIColor(0xff3824))]
|
||||
} else {
|
||||
peerRevealOptions = []
|
||||
}
|
||||
|
||||
let rightInset: CGFloat = 0.0
|
||||
|
||||
titleAttributedString = NSAttributedString(string: "\(item.session.appName) \(item.session.appVersion)", font: titleFont, textColor: UIColor.black)
|
||||
|
||||
var appString = ""
|
||||
if !item.session.deviceModel.isEmpty {
|
||||
appString = item.session.deviceModel
|
||||
}
|
||||
|
||||
if !item.session.platform.isEmpty {
|
||||
if !appString.isEmpty {
|
||||
appString += ", "
|
||||
}
|
||||
appString += item.session.platform
|
||||
}
|
||||
|
||||
if !item.session.systemVersion.isEmpty {
|
||||
if !appString.isEmpty {
|
||||
appString += ", "
|
||||
}
|
||||
appString += item.session.systemVersion
|
||||
}
|
||||
|
||||
appAttributedString = NSAttributedString(string: appString, font: textFont, textColor: UIColor.black)
|
||||
locationAttributedString = NSAttributedString(string: "\(item.session.ip) — \(item.session.country)", font: textFont, textColor: UIColor(0x6d6d6d))
|
||||
if item.session.isCurrent {
|
||||
labelAttributedString = NSAttributedString(string: "online", font: textFont, textColor: UIColor(0x007ee5))
|
||||
} else {
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
let dateText = stringForRelativeTimestamp(item.session.activityDate, relativeTo: timestamp)
|
||||
labelAttributedString = NSAttributedString(string: dateText, font: textFont, textColor: UIColor(0x6d6d6d))
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 15.0
|
||||
|
||||
var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)?
|
||||
|
||||
let editingOffset: CGFloat
|
||||
if item.editing {
|
||||
let sizeAndApply = editableControlLayout(75.0)
|
||||
editableControlSizeAndApply = sizeAndApply
|
||||
editingOffset = sizeAndApply.0.width
|
||||
} else {
|
||||
editingOffset = 0.0
|
||||
}
|
||||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
let (appLayout, appApply) = makeAppLayout(appAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
let (locationLayout, locationApply) = makeLocationLayout(locationAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), nil)
|
||||
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
let contentSize = CGSize(width: width, height: 75.0)
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
if !item.enabled {
|
||||
if currentDisabledOverlayNode == nil {
|
||||
currentDisabledOverlayNode = ASDisplayNode()
|
||||
currentDisabledOverlayNode?.backgroundColor = UIColor(white: 1.0, alpha: 0.5)
|
||||
}
|
||||
} else {
|
||||
currentDisabledOverlayNode = nil
|
||||
}
|
||||
|
||||
return (layout, { [weak self] animated in
|
||||
if let strongSelf = self {
|
||||
strongSelf.layoutParams = (item, width, neighbors)
|
||||
|
||||
let revealOffset = strongSelf.revealOffset
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
if let currentDisabledOverlayNode = currentDisabledOverlayNode {
|
||||
if currentDisabledOverlayNode != strongSelf.disabledOverlayNode {
|
||||
strongSelf.disabledOverlayNode = currentDisabledOverlayNode
|
||||
strongSelf.addSubnode(currentDisabledOverlayNode)
|
||||
currentDisabledOverlayNode.alpha = 0.0
|
||||
transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0)
|
||||
currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))
|
||||
} else {
|
||||
transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)))
|
||||
}
|
||||
} else if let disabledOverlayNode = strongSelf.disabledOverlayNode {
|
||||
transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in
|
||||
disabledOverlayNode?.removeFromSupernode()
|
||||
})
|
||||
strongSelf.disabledOverlayNode = nil
|
||||
}
|
||||
|
||||
if let editableControlSizeAndApply = editableControlSizeAndApply {
|
||||
if strongSelf.editableControlNode == nil {
|
||||
let editableControlNode = editableControlSizeAndApply.1()
|
||||
editableControlNode.tapped = {
|
||||
if let strongSelf = self {
|
||||
strongSelf.setRevealOptionsOpened(true, animated: true)
|
||||
strongSelf.revealOptionsInteractivelyOpened()
|
||||
}
|
||||
}
|
||||
strongSelf.editableControlNode = editableControlNode
|
||||
strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode)
|
||||
let editableControlFrame = CGRect(origin: CGPoint(x: revealOffset, y: 0.0), size: editableControlSizeAndApply.0)
|
||||
editableControlNode.frame = editableControlFrame
|
||||
transition.animatePosition(node: editableControlNode, from: CGPoint(x: editableControlFrame.midX - editableControlFrame.size.width, y: editableControlFrame.midY))
|
||||
editableControlNode.alpha = 0.0
|
||||
transition.updateAlpha(node: editableControlNode, alpha: 1.0)
|
||||
}
|
||||
strongSelf.editableControlNode?.isHidden = !item.editable
|
||||
} else if let editableControlNode = strongSelf.editableControlNode {
|
||||
var editableControlFrame = editableControlNode.frame
|
||||
editableControlFrame.origin.x = -editableControlFrame.size.width
|
||||
strongSelf.editableControlNode = nil
|
||||
transition.updateAlpha(node: editableControlNode, alpha: 0.0)
|
||||
transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in
|
||||
editableControlNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
let _ = labelApply()
|
||||
let _ = titleApply()
|
||||
let _ = appApply()
|
||||
let _ = locationApply()
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
strongSelf.topStripeNode.isHidden = false
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = leftInset + editingOffset
|
||||
bottomStripeOffset = -separatorHeight
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
}
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
|
||||
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
|
||||
|
||||
transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - labelLayout.size.width - 15.0 - rightInset, y: 10.0), size: labelLayout.size))
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 10.0), size: titleLayout.size))
|
||||
transition.updateFrame(node: strongSelf.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 30.0), size: appLayout.size))
|
||||
transition.updateFrame(node: strongSelf.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 50.0), size: locationLayout.size))
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 75.0 + UIScreenPixel + UIScreenPixel))
|
||||
|
||||
strongSelf.setRevealOptions(peerRevealOptions)
|
||||
strongSelf.setRevealOptionsOpened(item.revealed, animated: animated)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
let leftInset: CGFloat = 15.0
|
||||
let width = self.bounds.size.width
|
||||
|
||||
let editingOffset: CGFloat
|
||||
if let editableControlNode = self.editableControlNode {
|
||||
editingOffset = editableControlNode.bounds.size.width
|
||||
var editableControlFrame = editableControlNode.frame
|
||||
editableControlFrame.origin.x = offset
|
||||
transition.updateFrame(node: editableControlNode, frame: editableControlFrame)
|
||||
} else {
|
||||
editingOffset = 0.0
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - self.labelNode.bounds.size.width - 15.0, y: self.labelNode.frame.minY), size: self.labelNode.bounds.size))
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
|
||||
transition.updateFrame(node: self.appNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.appNode.frame.minY), size: self.appNode.bounds.size))
|
||||
transition.updateFrame(node: self.locationNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.locationNode.frame.minY), size: self.locationNode.bounds.size))
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
if let (item, _, _) = self.layoutParams {
|
||||
item.setSessionIdWithRevealedOptions(item.session.hash, nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyClosed() {
|
||||
if let (item, _, _) = self.layoutParams {
|
||||
item.setSessionIdWithRevealedOptions(nil, item.session.hash)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionSelected(_ option: ItemListRevealOption) {
|
||||
self.setRevealOptionsOpened(false, animated: true)
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
|
||||
if let (item, _, _) = self.layoutParams {
|
||||
item.removeSession(item.session.hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
TelegramUI/NumericFormat.swift
Normal file
22
TelegramUI/NumericFormat.swift
Normal file
@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
|
||||
public func compactNumericCountString(_ count: Int) -> String {
|
||||
if count >= 1000 * 1000 {
|
||||
let remainder = (count % (1000 * 1000)) / (1000 * 100)
|
||||
if remainder != 0 {
|
||||
return "\(count / (1000 * 1000)),\(remainder)M"
|
||||
} else {
|
||||
return "\(count / (1000 * 1000))M"
|
||||
}
|
||||
} else if count >= 1000 {
|
||||
let remainder = (count % (1000)) / (100)
|
||||
if remainder != 0 {
|
||||
return "\(count / 1000),\(remainder)K"
|
||||
} else {
|
||||
return "\(count / 1000)K"
|
||||
}
|
||||
} else {
|
||||
return "\(count)"
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,16 +6,41 @@ func stringForTimestamp(day: Int32, month: Int32, year: Int32) -> String {
|
||||
return String(format: "%d.%02d.%02d", day, month, year - 100)
|
||||
}
|
||||
|
||||
func stringForTimestamp(day: Int32, month: Int32) -> String {
|
||||
return String(format: "%d.%02d", day, month)
|
||||
}
|
||||
|
||||
func shortStringForDayOfWeek(_ day: Int32) -> String {
|
||||
switch day {
|
||||
case 0:
|
||||
return "Sun"
|
||||
case 1:
|
||||
return "Mon"
|
||||
case 2:
|
||||
return "Tue"
|
||||
case 3:
|
||||
return "Wed"
|
||||
case 4:
|
||||
return "Thu"
|
||||
case 5:
|
||||
return "Fri"
|
||||
case 6:
|
||||
return "Sat"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func stringForTime(hours: Int32, minutes: Int32) -> String {
|
||||
return String(format: "%d:%02d", hours, minutes)
|
||||
}
|
||||
|
||||
enum UserPresenceDay {
|
||||
enum RelativeTimestampFormatDay {
|
||||
case today
|
||||
case yesterday
|
||||
}
|
||||
|
||||
func stringForUserPresence(day: UserPresenceDay, hours: Int32, minutes: Int32) -> String {
|
||||
func stringForUserPresence(day: RelativeTimestampFormatDay, hours: Int32, minutes: Int32) -> String {
|
||||
let dayString: String
|
||||
switch day {
|
||||
case .today:
|
||||
@ -64,6 +89,31 @@ func relativeUserPresenceStatus(_ presence: TelegramUserPresence, relativeTo tim
|
||||
}
|
||||
}
|
||||
|
||||
func stringForRelativeTimestamp(_ relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String {
|
||||
var t: time_t = time_t(relativeTimestamp)
|
||||
var timeinfo: tm = tm()
|
||||
localtime_r(&t, &timeinfo)
|
||||
|
||||
var now: time_t = time_t(timestamp)
|
||||
var timeinfoNow: tm = tm()
|
||||
localtime_r(&now, &timeinfoNow)
|
||||
|
||||
if timeinfo.tm_year != timeinfoNow.tm_year {
|
||||
return stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year)
|
||||
}
|
||||
|
||||
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
|
||||
if dayDifference > -7 {
|
||||
if dayDifference == 0 {
|
||||
return stringForTime(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min)
|
||||
} else {
|
||||
return shortStringForDayOfWeek(timeinfo.tm_wday)
|
||||
}
|
||||
} else {
|
||||
return stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1)
|
||||
}
|
||||
}
|
||||
|
||||
func stringAndActivityForUserPresence(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> (String, Bool) {
|
||||
switch presence.status {
|
||||
case .none:
|
||||
@ -97,7 +147,7 @@ func stringAndActivityForUserPresence(_ presence: TelegramUserPresence, relative
|
||||
|
||||
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
|
||||
if dayDifference == 0 || dayDifference == -1 {
|
||||
let day: UserPresenceDay
|
||||
let day: RelativeTimestampFormatDay
|
||||
if dayDifference == 0 {
|
||||
day = .today
|
||||
} else {
|
||||
|
||||
262
TelegramUI/PrivacyAndSecurityController.swift
Normal file
262
TelegramUI/PrivacyAndSecurityController.swift
Normal file
@ -0,0 +1,262 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
private final class PrivacyAndSecurityControllerArguments {
|
||||
let account: Account
|
||||
let openBlockedUsers: () -> Void
|
||||
let openLastSeenPrivacy: () -> Void
|
||||
let openGroupsPrivacy: () -> Void
|
||||
let openVoiceCallPrivacy: () -> Void
|
||||
let openPasscode: () -> Void
|
||||
let openTwoStepVerification: () -> Void
|
||||
let openActiveSessions: () -> Void
|
||||
let setupAccountAutoremove: () -> Void
|
||||
|
||||
init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping () -> Void, openActiveSessions: @escaping () -> Void, setupAccountAutoremove: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.openBlockedUsers = openBlockedUsers
|
||||
self.openLastSeenPrivacy = openLastSeenPrivacy
|
||||
self.openGroupsPrivacy = openGroupsPrivacy
|
||||
self.openVoiceCallPrivacy = openVoiceCallPrivacy
|
||||
self.openPasscode = openPasscode
|
||||
self.openTwoStepVerification = openTwoStepVerification
|
||||
self.openActiveSessions = openActiveSessions
|
||||
self.setupAccountAutoremove = setupAccountAutoremove
|
||||
}
|
||||
}
|
||||
|
||||
private enum PrivacyAndSecuritySection: Int32 {
|
||||
case privacy
|
||||
case security
|
||||
case account
|
||||
}
|
||||
|
||||
private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
|
||||
case privacyHeader
|
||||
case blockedPeers
|
||||
case lastSeenPrivacy(String)
|
||||
case groupPrivacy(String)
|
||||
case voiceCallPrivacy(String)
|
||||
case securityHeader
|
||||
case passcode
|
||||
case twoStepVerification
|
||||
case activeSessions
|
||||
case accountHeader
|
||||
case accountTimeout(String)
|
||||
case accountInfo
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .privacyHeader, .blockedPeers, .lastSeenPrivacy, .groupPrivacy, .voiceCallPrivacy:
|
||||
return PrivacyAndSecuritySection.privacy.rawValue
|
||||
case .securityHeader, .passcode, .twoStepVerification, .activeSessions:
|
||||
return PrivacyAndSecuritySection.security.rawValue
|
||||
case .accountHeader, .accountTimeout, .accountInfo:
|
||||
return PrivacyAndSecuritySection.account.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .privacyHeader:
|
||||
return 0
|
||||
case .blockedPeers:
|
||||
return 1
|
||||
case .lastSeenPrivacy:
|
||||
return 2
|
||||
case .groupPrivacy:
|
||||
return 3
|
||||
case .voiceCallPrivacy:
|
||||
return 4
|
||||
case .securityHeader:
|
||||
return 5
|
||||
case .passcode:
|
||||
return 6
|
||||
case .twoStepVerification:
|
||||
return 7
|
||||
case .activeSessions:
|
||||
return 8
|
||||
case .accountHeader:
|
||||
return 9
|
||||
case .accountTimeout:
|
||||
return 10
|
||||
case .accountInfo:
|
||||
return 11
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .privacyHeader, .blockedPeers, .securityHeader, .passcode, .twoStepVerification, .activeSessions, .accountHeader, .accountInfo:
|
||||
return lhs.stableId == rhs.stableId
|
||||
case let .lastSeenPrivacy(text):
|
||||
if case .lastSeenPrivacy(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .groupPrivacy(text):
|
||||
if case .groupPrivacy(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .voiceCallPrivacy(text):
|
||||
if case .voiceCallPrivacy(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .accountTimeout(text):
|
||||
if case .accountTimeout(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(_ arguments: PrivacyAndSecurityControllerArguments) -> ListViewItem {
|
||||
switch self {
|
||||
case .privacyHeader:
|
||||
return ItemListSectionHeaderItem(text: "PRIVACY", sectionId: self.section)
|
||||
case .blockedPeers:
|
||||
return ItemListDisclosureItem(title: "Blocked Users", label: "", sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openBlockedUsers()
|
||||
})
|
||||
case let .lastSeenPrivacy(text):
|
||||
return ItemListDisclosureItem(title: "Last Seen", label: text, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openLastSeenPrivacy()
|
||||
})
|
||||
case let .groupPrivacy(text):
|
||||
return ItemListDisclosureItem(title: "Groups", label: text, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openGroupsPrivacy()
|
||||
})
|
||||
case let .voiceCallPrivacy(text):
|
||||
return ItemListDisclosureItem(title: "Voice Calls", label: text, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openVoiceCallPrivacy()
|
||||
})
|
||||
case .securityHeader:
|
||||
return ItemListSectionHeaderItem(text: "SECURITY", sectionId: self.section)
|
||||
case .passcode:
|
||||
return ItemListDisclosureItem(title: "Passcode Lock", label: "", sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openPasscode()
|
||||
})
|
||||
case .twoStepVerification:
|
||||
return ItemListDisclosureItem(title: "Two-Step Verification", label: "", sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openTwoStepVerification()
|
||||
})
|
||||
case .activeSessions:
|
||||
return ItemListDisclosureItem(title: "Active Sessions", label: "", sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openActiveSessions()
|
||||
})
|
||||
case .accountHeader:
|
||||
return ItemListSectionHeaderItem(text: "DELETE MY ACCOUNT", sectionId: self.section)
|
||||
case let .accountTimeout(text):
|
||||
return ItemListDisclosureItem(title: "If Away For", label: text, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.setupAccountAutoremove()
|
||||
})
|
||||
case .accountInfo:
|
||||
return ItemListTextItem(text: "If you do not log in at least once within this period, your account will be deleted along with all groups, messages and contacts.", sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PrivacyAndSecurityControllerState: Equatable {
|
||||
init() {
|
||||
}
|
||||
|
||||
static func ==(lhs: PrivacyAndSecurityControllerState, rhs: PrivacyAndSecurityControllerState) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func privacyAndSecurityControllerEntries(state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?) -> [PrivacyAndSecurityEntry] {
|
||||
var entries: [PrivacyAndSecurityEntry] = []
|
||||
|
||||
entries.append(.privacyHeader)
|
||||
entries.append(.blockedPeers)
|
||||
entries.append(.lastSeenPrivacy(""))
|
||||
entries.append(.groupPrivacy(""))
|
||||
entries.append(.voiceCallPrivacy(""))
|
||||
|
||||
entries.append(.securityHeader)
|
||||
entries.append(.passcode)
|
||||
entries.append(.twoStepVerification)
|
||||
entries.append(.activeSessions)
|
||||
entries.append(.accountHeader)
|
||||
entries.append(.accountTimeout(""))
|
||||
entries.append(.accountInfo)
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func privacyAndSecurityController(account: Account) -> ViewController {
|
||||
let statePromise = ValuePromise(PrivacyAndSecurityControllerState(), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: PrivacyAndSecurityControllerState())
|
||||
let updateState: ((PrivacyAndSecurityControllerState) -> PrivacyAndSecurityControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let checkAddressNameDisposable = MetaDisposable()
|
||||
actionsDisposable.add(checkAddressNameDisposable)
|
||||
|
||||
let updateAddressNameDisposable = MetaDisposable()
|
||||
actionsDisposable.add(updateAddressNameDisposable)
|
||||
|
||||
let arguments = PrivacyAndSecurityControllerArguments(account: account, openBlockedUsers: {
|
||||
pushControllerImpl?(blockedPeersController(account: account))
|
||||
}, openLastSeenPrivacy: {
|
||||
|
||||
}, openGroupsPrivacy: {
|
||||
|
||||
}, openVoiceCallPrivacy: {
|
||||
|
||||
}, openPasscode: {
|
||||
|
||||
}, openTwoStepVerification: {
|
||||
|
||||
}, openActiveSessions: {
|
||||
pushControllerImpl?(recentSessionsController(account: account))
|
||||
}, setupAccountAutoremove: {
|
||||
|
||||
})
|
||||
|
||||
let privacySettings: Signal<AccountPrivacySettings?, NoError> = .single(nil) |> then(updatedAccountPrivacySettings(account: account) |> map { Optional($0) })
|
||||
|> deliverOnMainQueue
|
||||
|
||||
let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, privacySettings)
|
||||
|> map { state, privacySettings -> (ItemListControllerState, (ItemListNodeState<PrivacyAndSecurityEntry>, PrivacyAndSecurityEntry.ItemGenerationArguments)) in
|
||||
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
if privacySettings == nil {
|
||||
rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {})
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(title: "Privacy and Security", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: false)
|
||||
let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(state: state, privacySettings: privacySettings), style: .blocks, animateChanges: false)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(signal)
|
||||
controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
(controller?.navigationController as? NavigationController)?.pushViewController(c)
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
342
TelegramUI/RecentSessionsController.swift
Normal file
342
TelegramUI/RecentSessionsController.swift
Normal file
@ -0,0 +1,342 @@
|
||||
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
|
||||
|
||||
init(account: Account, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void) {
|
||||
self.account = account
|
||||
self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions
|
||||
self.removeSession = removeSession
|
||||
}
|
||||
}
|
||||
|
||||
private enum RecentSessionsSection: Int32 {
|
||||
case currentSession
|
||||
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
|
||||
case currentSession(RecentAccountSession)
|
||||
case terminateOtherSessions
|
||||
case currentSessionInfo
|
||||
|
||||
case otherSessionsHeader
|
||||
case session(index: Int32, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .currentSessionHeader, .currentSession, .terminateOtherSessions, .currentSessionInfo:
|
||||
return RecentSessionsSection.currentSession.rawValue
|
||||
case .otherSessionsHeader, .session:
|
||||
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 .currentSessionInfo:
|
||||
return .index(3)
|
||||
case .otherSessionsHeader:
|
||||
return .index(4)
|
||||
case let .session(_, session, _, _, _):
|
||||
return .session(session.hash)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .currentSessionHeader, .terminateOtherSessions, .currentSessionInfo, .otherSessionsHeader:
|
||||
return lhs.stableId == rhs.stableId
|
||||
case let .currentSession(session):
|
||||
if case .currentSession(session) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .session(index, session, enabled, editing, revealed):
|
||||
if case .session(index, session, enabled, editing, revealed) = rhs {
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
case .session:
|
||||
switch lhs {
|
||||
case let .session(lhsIndex, _, _, _, _):
|
||||
if case let .session(rhsIndex, _, _, _, _) = rhs {
|
||||
return lhsIndex <= rhsIndex
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(_ arguments: RecentSessionsControllerArguments) -> ListViewItem {
|
||||
switch self {
|
||||
case .currentSessionHeader:
|
||||
return ItemListSectionHeaderItem(text: "CURRENT SESSION", sectionId: self.section)
|
||||
case let .currentSession(session):
|
||||
return ItemListRecentSessionItem(session: session, enabled: true, editable: false, editing: false, revealed: false, sectionId: self.section, setSessionIdWithRevealedOptions: { _, _ in
|
||||
}, removeSession: { _ in
|
||||
})
|
||||
case .terminateOtherSessions:
|
||||
return ItemListActionItem(title: "Terminate all other sessions", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
|
||||
})
|
||||
case .currentSessionInfo:
|
||||
return ItemListTextItem(text: "Logs out all devices except for this one.", sectionId: self.section)
|
||||
case .otherSessionsHeader:
|
||||
return ItemListSectionHeaderItem(text: "ACTIVE SESSIONS", sectionId: self.section)
|
||||
case let .session(_, session, enabled, editing, revealed):
|
||||
return ItemListRecentSessionItem(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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct RecentSessionsControllerState: Equatable {
|
||||
let editing: Bool
|
||||
let sessionIdWithRevealedOptions: Int64?
|
||||
let removingSessionId: Int64?
|
||||
|
||||
init() {
|
||||
self.editing = false
|
||||
self.sessionIdWithRevealedOptions = nil
|
||||
self.removingSessionId = nil
|
||||
}
|
||||
|
||||
init(editing: Bool, sessionIdWithRevealedOptions: Int64?, removingSessionId: Int64?) {
|
||||
self.editing = editing
|
||||
self.sessionIdWithRevealedOptions = sessionIdWithRevealedOptions
|
||||
self.removingSessionId = removingSessionId
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func withUpdatedEditing(_ editing: Bool) -> RecentSessionsControllerState {
|
||||
return RecentSessionsControllerState(editing: editing, sessionIdWithRevealedOptions: self.sessionIdWithRevealedOptions, removingSessionId: self.removingSessionId)
|
||||
}
|
||||
|
||||
func withUpdatedSessionIdWithRevealedOptions(_ sessionIdWithRevealedOptions: Int64?) -> RecentSessionsControllerState {
|
||||
return RecentSessionsControllerState(editing: self.editing, sessionIdWithRevealedOptions: sessionIdWithRevealedOptions, removingSessionId: self.removingSessionId)
|
||||
}
|
||||
|
||||
func withUpdatedRemovingSessionId(_ removingSessionId: Int64?) -> RecentSessionsControllerState {
|
||||
return RecentSessionsControllerState(editing: self.editing, sessionIdWithRevealedOptions: self.sessionIdWithRevealedOptions, removingSessionId: removingSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
private func recentSessionsControllerEntries(state: RecentSessionsControllerState, sessions: [RecentAccountSession]?) -> [RecentSessionsEntry] {
|
||||
var entries: [RecentSessionsEntry] = []
|
||||
|
||||
if let sessions = sessions {
|
||||
var existingSessionIds = Set<Int64>()
|
||||
entries.append(.currentSessionHeader)
|
||||
if let index = sessions.index(where: { $0.hash == 0 }) {
|
||||
existingSessionIds.insert(sessions[index].hash)
|
||||
entries.append(.currentSession(sessions[index]))
|
||||
}
|
||||
entries.append(.terminateOtherSessions)
|
||||
entries.append(.currentSessionInfo)
|
||||
|
||||
if sessions.count > 1 {
|
||||
entries.append(.otherSessionsHeader)
|
||||
|
||||
let filteredSessions: [RecentAccountSession] = sessions.sorted(by: { lhs, rhs in
|
||||
return lhs.activityDate > rhs.activityDate
|
||||
})
|
||||
|
||||
for i in 0 ..< filteredSessions.count {
|
||||
if !existingSessionIds.contains(sessions[i].hash) {
|
||||
existingSessionIds.insert(sessions[i].hash)
|
||||
entries.append(.session(index: Int32(i), session: sessions[i], enabled: state.removingSessionId != sessions[i].hash, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == sessions[i].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 sessionsPromise = Promise<[RecentAccountSession]?>(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
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingSessionId(sessionId)
|
||||
}
|
||||
|
||||
let applySessions: Signal<Void, NoError> = sessionsPromise.get()
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { sessions -> Signal<Void, NoError> 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) |> deliverOnMainQueue).start(error: { _ in
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingSessionId(nil)
|
||||
}
|
||||
}, completed: {
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingSessionId(nil)
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
let sessionsSignal: Signal<[RecentAccountSession]?, NoError> = .single(nil) |> then(requestRecentAccountSessions(account: account) |> map { Optional($0) })
|
||||
|
||||
sessionsPromise.set(sessionsSignal)
|
||||
|
||||
var previousSessions: [RecentAccountSession]?
|
||||
|
||||
let signal = combineLatest(statePromise.get(), sessionsPromise.get())
|
||||
|> deliverOnMainQueue
|
||||
|> map { state, sessions -> (ItemListControllerState, (ItemListNodeState<RecentSessionsEntry>, RecentSessionsEntry.ItemGenerationArguments)) in
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
if let sessions = sessions, !sessions.isEmpty {
|
||||
if state.editing {
|
||||
rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: {
|
||||
updateState { state in
|
||||
return state.withUpdatedEditing(false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
|
||||
updateState { state in
|
||||
return state.withUpdatedEditing(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
if sessions == nil {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem()
|
||||
}
|
||||
|
||||
let previous = previousSessions
|
||||
previousSessions = sessions
|
||||
|
||||
let controllerState = ItemListControllerState(title: "Active Sessions", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
|
||||
let listState = ItemListNodeState(entries: recentSessionsControllerEntries(state: state, sessions: sessions), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && sessions != nil && previous!.count >= sessions!.count)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(signal)
|
||||
presentControllerImpl = { [weak controller] c, p in
|
||||
if let controller = controller {
|
||||
controller.present(c, in: .window, with: p)
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
@ -203,7 +203,7 @@ private enum SettingsEntry: ItemListNodeEntry {
|
||||
})
|
||||
case .privacyAndSecurity:
|
||||
return ItemListDisclosureItem(title: "Privacy and Security", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
|
||||
|
||||
arguments.pushController(privacyAndSecurityController(account: arguments.account))
|
||||
})
|
||||
case .dataAndStorage:
|
||||
return ItemListDisclosureItem(title: "Data and Storage", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: {
|
||||
@ -392,6 +392,7 @@ public func settingsController(account: Account, accountManager: AccountManager)
|
||||
}
|
||||
|
||||
let controller = ItemListController(signal)
|
||||
controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
|
||||
controller.tabBarItem.title = "Settings"
|
||||
controller.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconSettings")?.precomposed()
|
||||
controller.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconSettingsSelected")?.precomposed()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user