no message

This commit is contained in:
Peter 2017-03-08 01:03:03 +03:00
parent be625d8f96
commit c5d34b17f0
21 changed files with 1495 additions and 64 deletions

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

View File

@ -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 */,

View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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)"
}
}

View File

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

View 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
}

View 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
}

View File

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