From 702d19a07d91ead36bfd9e18dceaaaa9628bbe4f Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 11 Oct 2016 22:25:54 +0200 Subject: [PATCH] no message --- TelegramUI.xcodeproj/project.pbxproj | 17 ++ TelegramUI/ChannelInfoEntries.swift | 4 +- .../ChatChannelSubscriberInputPanelNode.swift | 6 +- TelegramUI/ChatHoleItem.swift | 2 +- TelegramUI/ChatListItem.swift | 17 +- TelegramUI/ChatMediaActionSheetRollItem.swift | 2 +- TelegramUI/ChatMessageActionItemNode.swift | 2 +- TelegramUI/ChatMessageBubbleContentNode.swift | 3 + TelegramUI/ChatMessageBubbleItemNode.swift | 12 +- TelegramUI/ChatMessageDateAndStatusNode.swift | 12 +- .../ChatMessageFileBubbleContentNode.swift | 4 + .../ChatMessageInteractiveFileNode.swift | 2 +- .../ChatMessageMediaBubbleContentNode.swift | 4 + .../ChatMessageSelectionInputPanelNode.swift | 4 +- .../ChatMessageTextBubbleContentNode.swift | 5 + .../ChatMessageWebpageBubbleContentNode.swift | 17 +- TelegramUI/ChatTextInputPanelNode.swift | 2 +- TelegramUI/ChatTitleView.swift | 139 +++++++++-- TelegramUI/ContactsController.swift | 221 ++++++++++++------ TelegramUI/ContactsPeerItem.swift | 111 ++++++--- TelegramUI/ContactsSearchContainerNode.swift | 2 +- TelegramUI/GalleryController.swift | 2 +- TelegramUI/GroupInfoEntries.swift | 135 ++++++++++- TelegramUI/ListMessageFileItemNode.swift | 6 +- TelegramUI/ListMessageSnippetItemNode.swift | 2 +- TelegramUI/PeerInfoActionItem.swift | 18 +- TelegramUI/PeerInfoAvatarAndNameItem.swift | 9 +- TelegramUI/PeerInfoController.swift | 25 +- TelegramUI/PeerInfoDisclosureItem.swift | 16 ++ TelegramUI/PeerInfoEntries.swift | 33 ++- TelegramUI/PeerInfoPeerActionItem.swift | 18 +- TelegramUI/PeerInfoPeerItem.swift | 78 ++++++- TelegramUI/PeerInfoTextWithLabelItem.swift | 8 +- ...PeerMediaCollectionModeSelectionNode.swift | 4 +- TelegramUI/PeerPresenceStatusManager.swift | 32 +++ TelegramUI/PresenceStrings.swift | 140 +++++++++++ TelegramUI/ReplyAccessoryPanelNode.swift | 4 +- TelegramUI/SearchBarNode.swift | 2 +- TelegramUI/SettingsAccountInfoItem.swift | 2 +- TelegramUI/TextNode.swift | 1 + TelegramUI/UserInfoEntries.swift | 40 +++- 41 files changed, 968 insertions(+), 195 deletions(-) create mode 100644 TelegramUI/PeerPresenceStatusManager.swift create mode 100644 TelegramUI/PresenceStrings.swift diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 1e587526a0..9701f901f6 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -40,6 +40,8 @@ D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */; }; D0B843D91DAAAA0C005F29E1 /* PeerInfoPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D81DAAAA0C005F29E1 /* PeerInfoPeerItem.swift */; }; D0B843DB1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843DA1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift */; }; + D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */; }; + D0B844581DAC44E8005F29E1 /* PeerPresenceStatusManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */; }; D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */; }; D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */; }; D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */; }; @@ -252,6 +254,8 @@ D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoEntries.swift; sourceTree = ""; }; D0B843D81DAAAA0C005F29E1 /* PeerInfoPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoPeerItem.swift; sourceTree = ""; }; D0B843DA1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoPeerActionItem.swift; sourceTree = ""; }; + D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = ""; }; + D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPresenceStatusManager.swift; sourceTree = ""; }; D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputPanelNode.swift; sourceTree = ""; }; D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateInputPanels.swift; sourceTree = ""; }; D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionInputPanelNode.swift; sourceTree = ""; }; @@ -544,6 +548,14 @@ name = "Peer Media Collection"; sourceTree = ""; }; + D0B844541DAC3ADF005F29E1 /* Strings */ = { + isa = PBXGroup; + children = ( + D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */, + ); + name = Strings; + sourceTree = ""; + }; D0BA6F811D784C3A0034826E /* Input Panels */ = { isa = PBXGroup; children = ( @@ -1007,8 +1019,10 @@ D0F69E911D6B8C8E0046BCD6 /* Utils */ = { isa = PBXGroup; children = ( + D0B844541DAC3ADF005F29E1 /* Strings */, D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */, D0F69E941D6B8C9B0046BCD6 /* WebP.swift */, + D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */, ); name = Utils; sourceTree = ""; @@ -1197,6 +1211,7 @@ D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */, D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */, D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */, + D0B844581DAC44E8005F29E1 /* PeerPresenceStatusManager.swift in Sources */, D0D2686C1D788F8200C422DA /* NavigationAccessoryPanelNode.swift in Sources */, D07A7DA51D95783C005BCD27 /* ListMessageNode.swift in Sources */, D0F69E341D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift in Sources */, @@ -1247,6 +1262,7 @@ D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */, D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */, D0DF0CA61D82BCE0008AEB01 /* MentionsTableCell.swift in Sources */, + D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */, D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */, D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */, D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */, @@ -1600,6 +1616,7 @@ "$(PROJECT_DIR)/third-party/FFmpeg-iOS/lib", ); OTHER_LDFLAGS = "-ObjC"; + OTHER_SWIFT_FLAGS = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/TelegramUI/ChannelInfoEntries.swift b/TelegramUI/ChannelInfoEntries.swift index 69d75cc085..c2679d9daf 100644 --- a/TelegramUI/ChannelInfoEntries.swift +++ b/TelegramUI/ChannelInfoEntries.swift @@ -44,8 +44,8 @@ enum ChannelInfoEntry: PeerInfoEntry { } } - var stableId: Int { - return self.sortIndex + var stableId: PeerInfoEntryStableId { + return IntPeerInfoEntryStableId(value: self.sortIndex) } func isEqual(to: PeerInfoEntry) -> Bool { diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index e9cad531fb..843c1e3c3b 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -15,13 +15,13 @@ private enum SubscriberAction { private func titleAndColorForAction(_ action: SubscriberAction) -> (String, UIColor) { switch action { case .join: - return ("Join", UIColor(0x1195f2)) + return ("Join", UIColor(0x007ee5)) case .kicked: return ("Join", UIColor.gray) case .muteNotifications: - return ("Mute", UIColor(0x1195f2)) + return ("Mute", UIColor(0x007ee5)) case .unmuteNotifications: - return ("Unmute", UIColor(0x1195f2)) + return ("Unmute", UIColor(0x007ee5)) } } diff --git a/TelegramUI/ChatHoleItem.swift b/TelegramUI/ChatHoleItem.swift index 540dfc4fa9..5583f9044a 100644 --- a/TelegramUI/ChatHoleItem.swift +++ b/TelegramUI/ChatHoleItem.swift @@ -40,7 +40,7 @@ class ChatHoleItemNode: ListViewItemNode { super.init(layerBacked: true) - self.backgroundNode.image = backgroundImage(color: UIColor(0x1195f2)) + self.backgroundNode.image = backgroundImage(color: UIColor(0x007ee5)) self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index c056b157f1..5c665c2da0 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -94,7 +94,7 @@ private func generateBadgeBackgroundImage(active: Bool) -> UIImage? { return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) if active { - context.setFillColor(UIColor(0x1195f2).cgColor) + context.setFillColor(UIColor(0x007ee5).cgColor) } else { context.setFillColor(UIColor(0xbbbbbb).cgColor) } @@ -116,6 +116,7 @@ class ChatListItemNode: ListViewItemNode { var combinedReadState: CombinedPeerReadState? var notificationSettings: PeerNotificationSettings? + private let backgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode let avatarNode: AvatarNode @@ -132,6 +133,11 @@ class ChatListItemNode: ListViewItemNode { var relativePosition: (first: Bool, last: Bool) = (false, false) required init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.backgroundColor = .white + self.avatarNode = AvatarNode(font: Font.regular(24.0)) self.avatarNode.isLayerBacked = true @@ -185,6 +191,7 @@ class ChatListItemNode: ListViewItemNode { super.init(layerBacked: false, dynamicBounce: false) + self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) self.addSubnode(self.avatarNode) self.addSubnode(self.contentNode) @@ -220,6 +227,7 @@ class ChatListItemNode: ListViewItemNode { let size = self.bounds.size let insets = self.insets + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top - separatorHeight), size: CGSize(width: size.width, height: size.height + separatorHeight)) } @@ -469,7 +477,10 @@ class ChatListItemNode: ListViewItemNode { } override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) - self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) } } diff --git a/TelegramUI/ChatMediaActionSheetRollItem.swift b/TelegramUI/ChatMediaActionSheetRollItem.swift index 855dca4c11..c571fe44bd 100644 --- a/TelegramUI/ChatMediaActionSheetRollItem.swift +++ b/TelegramUI/ChatMediaActionSheetRollItem.swift @@ -26,7 +26,7 @@ private final class ChatMediaActionSheetRollItemNode: ActionSheetItemNode, PHPho self.label = UILabel() self.label.backgroundColor = nil self.label.isOpaque = false - self.label.textColor = UIColor(0x1195f2) + self.label.textColor = UIColor(0x007ee5) self.label.text = "Photo or Video" self.label.font = Font.regular(20.0) self.label.sizeToFit() diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index f14c117828..ac756b2b6d 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -33,7 +33,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { super.init(layerBacked: false) - self.backgroundNode.image = backgroundImage(color: UIColor(0x1195f2)) + self.backgroundNode.image = backgroundImage(color: UIColor(0x007ee5)) self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) } diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index 171bd5c333..951515e808 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -51,6 +51,9 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func animateAdded(_ currentTimestamp: Double, duration: Double) { } + func animateRemoved(_ currentTimestamp: Double, duration: Double) { + } + func animateInsertionIntoBubble(_ duration: Double) { } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 00b008e556..ac215057b8 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -187,6 +187,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + for contentNode in self.contentNodes { + contentNode.animateRemoved(currentTimestamp, duration: duration) + } + } + override func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) @@ -347,7 +357,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { headerSize.height += 4.0 } - let inlineBotNameColor = incoming ? UIColor(0x1195f2) : UIColor(0x00a700) + let inlineBotNameColor = incoming ? UIColor(0x007ee5) : UIColor(0x00a700) let attributedString: NSAttributedString if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let inlineBotNameString = inlineBotNameString { diff --git a/TelegramUI/ChatMessageDateAndStatusNode.swift b/TelegramUI/ChatMessageDateAndStatusNode.swift index 6f2cf2858e..b315781695 100644 --- a/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -190,10 +190,10 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { let checkSize = checkFullImage!.size - checkSentFrame = CGRect(origin: CGPoint(x: leftInset + date.size.width + 5.0 + statusWidth - checkSize.width - (read ? 0.0 : 2.5), y: 3.0), size: checkSize) if read { - checkReadFrame = CGRect(origin: CGPoint(x: checkSentFrame!.origin.x - 6.0, y: checkSentFrame!.origin.y), size: checkSize) + checkReadFrame = CGRect(origin: CGPoint(x: leftInset + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0), size: checkSize) } + checkSentFrame = CGRect(origin: CGPoint(x: leftInset + date.size.width + 5.0 + statusWidth - checkSize.width - 6.0, y: 3.0), size: checkSize) case .Failed: statusWidth = 0.0 @@ -248,15 +248,11 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { if let checkSentNode = checkSentNode, let checkReadNode = checkReadNode { var animateSentNode = false if strongSelf.checkSentNode == nil { + checkSentNode.image = loadedCheckFullImage strongSelf.checkSentNode = checkSentNode strongSelf.addSubnode(checkSentNode) animateSentNode = animated } - if checkReadFrame != nil { - checkSentNode.image = loadedCheckPartialImage - } else { - checkSentNode.image = loadedCheckFullImage - } if let checkSentFrame = checkSentFrame { if checkSentNode.isHidden { @@ -271,10 +267,10 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { var animateReadNode = false if strongSelf.checkReadNode == nil { animateReadNode = animated + checkReadNode.image = loadedCheckPartialImage strongSelf.checkReadNode = checkReadNode strongSelf.addSubnode(checkReadNode) } - checkReadNode.image = loadedCheckFullImage if let checkReadFrame = checkReadFrame { if checkReadNode.isHidden { diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index f1d44447a5..fbed759b15 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -70,4 +70,8 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { override func animateAdded(_ currentTimestamp: Double, duration: Double) { self.interactiveFileNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.interactiveFileNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } } diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index ebd76a5026..ad293ab5ee 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -181,7 +181,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { strongSelf.fetchStatus = status if strongSelf.progressNode == nil { - let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(incoming ? 0x1195f2 : 0x3fc33b), foregroundColor: incoming ? UIColor.white : UIColor(0xe1ffc7), icon: incoming ? fileIconIncomingImage : fileIconOutgoingImage)) + let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(incoming ? 0x007ee5 : 0x3fc33b), foregroundColor: incoming ? UIColor.white : UIColor(0xe1ffc7), icon: incoming ? fileIconIncomingImage : fileIconOutgoingImage)) strongSelf.progressNode = progressNode progressNode.frame = progressFrame strongSelf.addSubnode(progressNode) diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index f0562d975f..7a67328354 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -81,6 +81,10 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.interactiveImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + override func transitionNode(media: Media) -> ASDisplayNode? { if let currentMedia = self.media, currentMedia.isEqual(media) { return self.interactiveImageNode diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index 158573a072..d4bece6e70 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -46,9 +46,9 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.deleteButton = UIButton() self.forwardButton = UIButton() - self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: UIColor(0x1195f2)), for: [.normal]) + self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: UIColor(0x007ee5)), for: [.normal]) self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: UIColor(0xdededf)), for: [.disabled]) - self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: UIColor(0x1195f2)), for: [.normal]) + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: UIColor(0x007ee5)), for: [.normal]) self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: UIColor(0xdededf)), for: [.disabled]) super.init() diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index fda8677c16..7682f5c1d5 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -180,4 +180,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 00453adcdb..248112e129 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -381,11 +381,24 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.inlineImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } override func animateAdded(_ currentTimestamp: Double, duration: Double) { - self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.lineNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.inlineImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.lineNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.inlineImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) } override func animateInsertionIntoBubble(_ duration: Double) { diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 901bea25f1..06a5d0765f 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -107,7 +107,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.sendButton.titleLabel?.font = Font.medium(17.0) self.sendButton.contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 6.0, bottom: 8.0, right: 6.0) - self.sendButton.setTitleColor(UIColor(0x1195f2), for: []) + self.sendButton.setTitleColor(UIColor(0x007ee5), for: []) self.sendButton.setTitleColor(UIColor.gray, for: [.highlighted]) self.sendButton.setTitle("Send", for: []) self.sendButton.sizeToFit() diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index 145fd8da5f..cc70e109b9 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -3,33 +3,110 @@ import AsyncDisplayKit import Display import Postbox import TelegramCore +import SwiftSignalKit final class ChatTitleView: UIView { private let titleNode: ASTextNode private let infoNode: ASTextNode + private let button: HighlightTrackingButton + + private var presenceManager: PeerPresenceStatusManager? + + var pressed: (() -> Void)? var peerView: PeerView? { didSet { if let peerView = self.peerView, let peer = peerView.peers[peerView.peerId] { - self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: UIColor.black) + let string = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: UIColor.black) - if let user = peer as? TelegramUser { - self.infoNode.attributedText = NSAttributedString(string: "last seen recently", font: Font.regular(13.0), textColor: UIColor(0x787878)) - } else if let group = peer as? TelegramGroup { - self.infoNode.attributedText = NSAttributedString(string: "\(group.participantCount) members", font: Font.regular(13.0), textColor: UIColor(0x787878)) - } else if let channel = peer as? TelegramChannel { - if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { - self.infoNode.attributedText = NSAttributedString(string: "\(memberCount) members", font: Font.regular(13.0), textColor: UIColor(0x787878)) - } else { - switch channel.info { - case .group: - self.infoNode.attributedText = NSAttributedString(string: "group", font: Font.regular(13.0), textColor: UIColor(0x787878)) - case .broadcast: - self.infoNode.attributedText = NSAttributedString(string: "channel", font: Font.regular(13.0), textColor: UIColor(0x787878)) + if self.titleNode.attributedText == nil || !self.titleNode.attributedText!.isEqual(to: string) { + self.titleNode.attributedText = string + self.setNeedsLayout() + } + + self.updateStatus() + } + } + } + + private func updateStatus() { + var shouldUpdateLayout = false + if let peerView = self.peerView, let peer = peerView.peers[peerView.peerId] { + if let user = peer as? TelegramUser { + if let presence = peerView.peerPresences[peerView.peerId] as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? UIColor(0x007ee5) : UIColor(0x787878)) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: attributedString) { + self.infoNode.attributedText = attributedString + shouldUpdateLayout = true + } + + self.presenceManager?.reset(presence: presence) + } else { + let string = NSAttributedString(string: "offline", 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 + } + } + } else if let group = peer as? TelegramGroup { + var onlineCount = 0 + if let cachedGroupData = peerView.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + for participant in participants.participants { + if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { + let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) + switch relativeStatus { + case .online: + onlineCount += 1 + default: + break + } } } } - + 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: "\(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)) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } + } 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)) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } else { + switch channel.info { + case .group: + let string = NSAttributedString(string: "group", 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 + } + case .broadcast: + let string = NSAttributedString(string: "channel", 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 + } + } + } + } + + if shouldUpdateLayout { self.setNeedsLayout() } } @@ -48,10 +125,34 @@ final class ChatTitleView: UIView { self.infoNode.truncationMode = .byTruncatingTail self.infoNode.isOpaque = false + self.button = HighlightTrackingButton() + super.init(frame: frame) self.addSubnode(self.titleNode) self.addSubnode(self.infoNode) + self.addSubview(self.button) + + self.presenceManager = PeerPresenceStatusManager(update: { [weak self] in + self?.updateStatus() + }) + + self.button.addTarget(self, action: #selector(buttonPressed), for: [.touchUpInside]) + self.button.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.infoNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode.alpha = 0.4 + strongSelf.infoNode.alpha = 0.4 + } else { + strongSelf.titleNode.alpha = 1.0 + strongSelf.infoNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.infoNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } } required init?(coder aDecoder: NSCoder) { @@ -63,6 +164,8 @@ final class ChatTitleView: UIView { let size = self.bounds.size + self.button.frame = CGRect(origin: CGPoint(), size: size) + if size.height > 40.0 { let titleSize = self.titleNode.measure(size) let infoSize = self.infoNode.measure(size) @@ -83,4 +186,10 @@ final class ChatTitleView: UIView { self.infoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - combinedWidth) / 2.0 + titleSize.width + titleInfoSpacing), y: floor((size.height - infoSize.height) / 2.0)), size: infoSize) } } + + @objc func buttonPressed() { + if let pressed = self.pressed { + pressed() + } + } } diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index 91abf60574..5085b81623 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -55,7 +55,7 @@ private func ==(lhs: ContactsControllerEntryId, rhs: ContactsControllerEntryId) private enum ContactsEntry: Comparable, Identifiable { case search case vcard(Peer) - case peer(Peer) + case peer(Peer, PeerPresence?) var stableId: ContactsControllerEntryId { switch self { @@ -63,10 +63,27 @@ private enum ContactsEntry: Comparable, Identifiable { return .search case .vcard: return .vcard - case let .peer(peer): + case let .peer(peer, _): return .peerId(peer.id.toInt64()) } } + + func item(account: Account, index: PeerNameIndex, interaction: ContactsControllerInteraction) -> ListViewItem { + switch self { + case .search: + return ChatListSearchItem(placeholder: "Search contacts", activate: { + interaction.activateSearch() + }) + case let .vcard(peer): + return ContactsVCardItem(account: account, peer: peer, action: { peer in + interaction.openPeer(peer.id) + }) + case let .peer(peer, presence): + return ContactsPeerItem(account: account, peer: peer, presence: presence, index: nil, action: { _ in + interaction.openPeer(peer.id) + }) + } + } } private func ==(lhs: ContactsEntry, rhs: ContactsEntry) -> Bool { @@ -85,10 +102,20 @@ private func ==(lhs: ContactsEntry, rhs: ContactsEntry) -> Bool { default: return false } - case let .peer(lhsPeer): + case let .peer(lhsPeer, lhsPresence): switch rhs { - case let .peer(rhsPeer): - return lhsPeer.id == rhsPeer.id + case let .peer(rhsPeer, rhsPresence): + if lhsPeer.id != rhsPeer.id { + return false + } + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if !lhsPresence.isEqual(to: rhsPresence) { + return false + } + } else if (lhsPresence != nil) != (rhsPresence != nil) { + return false + } + return true default: return false } @@ -96,28 +123,83 @@ private func ==(lhs: ContactsEntry, rhs: ContactsEntry) -> Bool { } private func <(lhs: ContactsEntry, rhs: ContactsEntry) -> Bool { - return lhs.stableId < rhs.stableId + switch lhs { + case .search: + return true + case .vcard: + switch rhs { + case .search, .vcard: + return false + case .peer: + return true + } + case let .peer(lhsPeer, lhsPresence): + switch rhs { + case .search: + return false + case .vcard: + return false + case let .peer(rhsPeer, rhsPresence): + if let lhsPresence = lhsPresence as? TelegramUserPresence, let rhsPresence = rhsPresence as? TelegramUserPresence { + if lhsPresence.status < rhsPresence.status { + return false + } else if lhsPresence.status > rhsPresence.status { + return true + } + } else if let _ = lhsPresence { + return true + } else if let _ = rhsPresence { + return false + } + return lhsPeer.id < rhsPeer.id + } + } } -private func entriesForView(_ view: ContactPeersView) -> [ContactsEntry] { +private func contactListEntries(_ view: ContactPeersView) -> [ContactsEntry] { var entries: [ContactsEntry] = [] entries.append(.search) if let peer = view.accountPeer { entries.append(.vcard(peer)) } for peer in view.peers { - entries.append(.peer(peer)) + entries.append(.peer(peer, view.peerPresences[peer.id])) } + entries.sort() return entries } +private struct ContactsListTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private final class ContactsControllerInteraction { + let openPeer: (PeerId) -> Void + let activateSearch: () -> Void + + init(openPeer: @escaping (PeerId) -> Void, activateSearch: @escaping () -> Void) { + self.openPeer = openPeer + self.activateSearch = activateSearch + } +} + +private func preparedContactsListTransition(account: Account, index: PeerNameIndex, from fromEntries: [ContactsEntry], to toEntries: [ContactsEntry], interaction: ContactsControllerInteraction) -> ContactsListTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, index: index, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, index: index, interaction: interaction), directionHint: nil) } + + return ContactsListTransition(deletions: deletions, insertions: insertions, updates: updates) +} + public class ContactsController: ViewController { private let queue = Queue() private let account: Account - private let disposable = MetaDisposable() - - private var entries: [ContactsEntry] = [] + private let transitionDisposable = MetaDisposable() private var contactsNode: ContactsControllerNode { return self.displayNode as! ContactsControllerNode @@ -125,6 +207,14 @@ public class ContactsController: ViewController { private let index: PeerNameIndex = .lastNameFirst + private var _ready = Promise() + override public var ready: Promise { + return self._ready + } + private var didSetReady = false + + private let previousEntries = Atomic<[ContactsEntry]?>(value: nil) + public init(account: Account) { self.account = account @@ -135,12 +225,8 @@ public class ContactsController: ViewController { self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconContacts") self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconContactsSelected") - self.disposable.set((account.postbox.contactPeersView(index: self.index, accountPeerId: account.peerId) |> deliverOn(self.queue)).start(next: { [weak self] view in - self?.updateView(view) - })) - self.scrollToTop = { [weak self] in - if let strongSelf = self, !strongSelf.entries.isEmpty { + if let strongSelf = self { strongSelf.contactsNode.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, completion: { _ in }) } } @@ -151,7 +237,7 @@ public class ContactsController: ViewController { } deinit { - self.disposable.dispose() + self.transitionDisposable.dispose() } override public func loadDisplayNode() { @@ -172,68 +258,61 @@ public class ContactsController: ViewController { self.displayNodeDidLoad() } + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + let interaction = ContactsControllerInteraction(openPeer: { [weak self] peerId in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId)) + } + }, activateSearch: { [weak self] in + self?.activateSearch() + }) + + let account = self.account + let index = self.index + let previousEntries = self.previousEntries + let transition = account.postbox.contactPeersView(index: self.index, accountPeerId: account.peerId) + |> map { view -> (ContactsListTransition, Bool, Bool) in + let entries = contactListEntries(view) + let previous = previousEntries.swap(entries) + return (preparedContactsListTransition(account: account, index: index, from: previous ?? [], to: entries, interaction: interaction), previous == nil, previous != nil) + } + |> deliverOnMainQueue + + self.transitionDisposable.set(transition.start(next: { [weak self] (transition, firstTime, animated) in + self?.enqueueTransition(transition, firstTime: firstTime, animated: animated) + })) + } + + override public func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.transitionDisposable.set(nil) + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) } - private func updateView(_ view: ContactPeersView) { - assert(self.queue.isCurrent()) - - let previousEntries = self.entries - let updatedEntries = entriesForView(view) - - let (deleteIndices, indicesAndItems) = mergeListsStable(leftList: previousEntries, rightList: updatedEntries) - - self.entries = updatedEntries - - var adjustedDeleteIndices: [ListViewDeleteItem] = [] - if deleteIndices.count != 0 { - for index in deleteIndices { - adjustedDeleteIndices.append(ListViewDeleteItem(index: index, directionHint: nil)) - } + private func enqueueTransition(_ transition: ContactsListTransition, firstTime: Bool, animated: Bool) { + var options = ListViewDeleteAndInsertOptions() + if firstTime { + options.insert(.Synchronous) + options.insert(.LowLatency) + } else if animated { + options.insert(.AnimateInsertion) } - - var adjustedIndicesAndItems: [ListViewInsertItem] = [] - for (index, entry, previousIndex) in indicesAndItems { - switch entry { - case .search: - adjustedIndicesAndItems.append(ListViewInsertItem(index: index, previousIndex: previousIndex, item: ChatListSearchItem(placeholder: "Search contacts", activate: { [weak self] in - self?.activateSearch() - }), directionHint: nil)) - case let .vcard(peer): - adjustedIndicesAndItems.append(ListViewInsertItem(index: index, previousIndex: previousIndex, item: ContactsVCardItem(account: self.account, peer: peer, action: { [weak self] _ in - if let strongSelf = self { - strongSelf.entrySelected(entry) - strongSelf.contactsNode.listView.clearHighlightAnimated(true) - } - }), directionHint: nil)) - case let .peer(peer): - adjustedIndicesAndItems.append(ListViewInsertItem(index: index, previousIndex: previousIndex, item: ContactsPeerItem(account: self.account, peer: peer, index: self.index, action: { [weak self] _ in - if let strongSelf = self { - strongSelf.entrySelected(entry) - strongSelf.contactsNode.listView.clearHighlightAnimated(true) - } - }), directionHint: nil)) + self.contactsNode.listView.deleteAndInsertItems(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, completion: { [weak self] _ in + if let strongSelf = self { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf._ready.set(.single(true)) } - } - - DispatchQueue.main.async { - let options: ListViewDeleteAndInsertOptions = [] - - self.contactsNode.listView.deleteAndInsertItems(deleteIndices: adjustedDeleteIndices, insertIndicesAndItems: adjustedIndicesAndItems, updateIndicesAndItems: [], options: options, scrollToItem: nil, completion: { _ in - }) - } - } - - private func entrySelected(_ entry: ContactsEntry) { - if case let .peer(peer) = entry { - (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: peer.id)) - } - if case let .vcard(peer) = entry { - (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: peer.id)) - } + } + }) } private func activateSearch() { diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index e5b9f3f27a..3535978ee2 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -13,14 +13,16 @@ private let statusFont = Font.regular(13.0) class ContactsPeerItem: ListViewItem { let account: Account let peer: Peer + let presence: PeerPresence? let action: (Peer) -> Void let selectable: Bool = true let headerAccessoryItem: ListViewAccessoryItem? - init(account: Account, peer: Peer, index: PeerNameIndex?, action: @escaping (Peer) -> Void) { + init(account: Account, peer: Peer, presence: PeerPresence?, index: PeerNameIndex?, action: @escaping (Peer) -> Void) { self.account = account self.peer = peer + self.presence = presence self.action = action if let index = index { @@ -72,7 +74,7 @@ class ContactsPeerItem: ListViewItem { last = false } } - let (nodeLayout, nodeApply) = makeLayout(self.account, self.peer, width, first, last) + let (nodeLayout, nodeApply) = makeLayout(self, width, first, last) node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets @@ -101,7 +103,7 @@ class ContactsPeerItem: ListViewItem { } } - let (nodeLayout, apply) = layout(self.account, self.peer, width, first, last) + let (nodeLayout, apply) = layout(self, width, first, last) Queue.mainQueue().async { completion(nodeLayout, { apply() @@ -120,6 +122,7 @@ class ContactsPeerItem: ListViewItem { private let separatorHeight = 1.0 / UIScreen.main.scale class ContactsPeerItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode @@ -127,11 +130,16 @@ class ContactsPeerItemNode: ListViewItemNode { private let titleNode: TextNode private let statusNode: TextNode - private var account: Account? - private var peer: Peer? private var avatarState: (Account, Peer)? + private var peerPresenceManager: PeerPresenceStatusManager? + private var layoutParams: (ContactsPeerItem, CGFloat, Bool, Bool)? + required init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = .white + self.backgroundNode.isLayerBacked = true + self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = UIColor(0xc8c7cc) self.separatorNode.isLayerBacked = true @@ -148,18 +156,29 @@ class ContactsPeerItemNode: ListViewItemNode { super.init(layerBacked: false, dynamicBounce: false) + self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) self.addSubnode(self.avatarNode) self.addSubnode(self.titleNode) self.addSubnode(self.statusNode) + + self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in + if let strongSelf = self, let layoutParams = strongSelf.layoutParams { + let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2, layoutParams.3) + apply() + } + }) } override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self.account, self.peer, width, previousItem != nil, nextItem != nil) - self.contentSize = nodeLayout.contentSize - self.insets = nodeLayout.insets - nodeApply() + if let (item, _, _, _) = self.layoutParams { + self.layoutParams = (item, width, previousItem != nil, nextItem != nil) + let makeLayout = self.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(item, width, previousItem != nil, nextItem != nil) + self.contentSize = nodeLayout.contentSize + self.insets = nodeLayout.insets + nodeApply() + } } override func setHighlighted(_ highlighted: Bool, animated: Bool) { @@ -198,39 +217,45 @@ class ContactsPeerItemNode: ListViewItemNode { } } - func asyncLayout() -> (_ account: Account?, _ peer: Peer?, _ width: CGFloat, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ContactsPeerItem, _ width: CGFloat, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) - return { [weak self] account, peer, width, first, last in + return { [weak self] item, width, first, last in let leftInset: CGFloat = 65.0 let rightInset: CGFloat = 10.0 var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? - if let peer = peer { - if let user = peer as? TelegramUser { - if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { - let string = NSMutableAttributedString() - string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black)) - titleAttributedString = string - } else if let firstName = user.firstName, !firstName.isEmpty { - titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: UIColor.black) - } else if let lastName = user.lastName, !lastName.isEmpty { - titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: UIColor.black) - } else { - titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6)) - } - - statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6)) - } else if let group = peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) - } else if let channel = peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black) + let peer = item.peer + + if let user = peer as? TelegramUser { + if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black)) + string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black)) + titleAttributedString = string + } else if let firstName = user.firstName, !firstName.isEmpty { + titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: UIColor.black) + } else if let lastName = user.lastName, !lastName.isEmpty { + titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: UIColor.black) + } else { + titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6)) } + + if let presence = item.presence as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? UIColor(0x007ee5) : UIColor(0xa6a6a6)) + } else { + statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6)) + } + } else if let group = peer as? TelegramGroup { + titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) + } else if let channel = peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black) } let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), nil) @@ -241,11 +266,10 @@ class ContactsPeerItemNode: ListViewItemNode { return (nodeLayout, { [weak self] in if let strongSelf = self { - strongSelf.peer = peer - strongSelf.account = account + strongSelf.layoutParams = (item, width, first, last) - if let peer = peer, let account = account, strongSelf.avatarState == nil || strongSelf.avatarState!.0 !== account || !strongSelf.avatarState!.1.isEqual(peer) { - strongSelf.avatarNode.setPeer(account: account, peer: peer) + if strongSelf.avatarState == nil || strongSelf.avatarState!.0 !== item.account || !strongSelf.avatarState!.1.isEqual(peer) { + strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer) } strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)) @@ -257,9 +281,14 @@ class ContactsPeerItemNode: ListViewItemNode { strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 25.0), size: statusLayout.size) let topHighlightInset: CGFloat = first ? 0.0 : separatorHeight + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 65.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - 65.0), height: separatorHeight)) strongSelf.separatorNode.isHidden = last + + if let presence = item.presence as? TelegramUserPresence { + strongSelf.peerPresenceManager?.reset(presence: presence) + } } }) } @@ -269,4 +298,12 @@ class ContactsPeerItemNode: ListViewItemNode { let bounds = self.bounds accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) + } } diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index 79b54a721b..4f1b5cf23c 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -55,7 +55,7 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { for item in items { switch item { case let .peer(peer): - listItems.append(ContactsPeerItem(account: account, peer: peer, index: nil, action: { [weak self] peer in + listItems.append(ContactsPeerItem(account: account, peer: peer, presence: nil, index: nil, action: { [weak self] peer in if let openPeer = self?.openPeer { self?.listNode.clearHighlightAnimated(true) openPeer(peer.id) diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index e6d5410862..b9100e8f70 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -215,7 +215,7 @@ class GalleryController: ViewController { strongSelf.statusBar.style = .Black strongSelf.navigationBar.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0) strongSelf.navigationBar.foregroundColor = UIColor.black - strongSelf.navigationBar.accentColor = UIColor(0x1195f2) + strongSelf.navigationBar.accentColor = UIColor(0x007ee5) strongSelf.navigationBar.stripeColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor(0xbdbdc2) strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = false diff --git a/TelegramUI/GroupInfoEntries.swift b/TelegramUI/GroupInfoEntries.swift index b3414dfcef..3f2a255521 100644 --- a/TelegramUI/GroupInfoEntries.swift +++ b/TelegramUI/GroupInfoEntries.swift @@ -28,6 +28,27 @@ private enum GroupInfoSection: UInt32, PeerInfoSection { } } +enum GroupInfoMemberStatus { + case member + case admin +} + +private struct GroupPeerEntryStableId: PeerInfoEntryStableId { + let peerId: PeerId + + func isEqual(to: PeerInfoEntryStableId) -> Bool { + if let to = to as? GroupPeerEntryStableId, to.peerId == self.peerId { + return true + } else { + return false + } + } + + var hashValue: Int { + return self.peerId.hashValue + } +} + enum GroupInfoEntry: PeerInfoEntry { case info(peer: Peer?, cachedData: CachedPeerData?) case setGroupPhoto @@ -37,7 +58,7 @@ enum GroupInfoEntry: PeerInfoEntry { case notifications(settings: PeerNotificationSettings?) case usersHeader case addMember - case member(index: Int, peer: Peer?) + case member(index: Int, peerId: PeerId, peer: Peer?, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus) case leave var section: PeerInfoSection { @@ -132,11 +153,17 @@ enum GroupInfoEntry: PeerInfoEntry { } else { return false } - case let .member(lhsIndex, lhsPeer): - if case let .member(rhsIndex, rhsPeer) = entry { + case let .member(lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus): + if case let .member(rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus) = entry { if lhsIndex != rhsIndex { return false } + if lhsMemberStatus != rhsMemberStatus { + return false + } + if lhsPeerId != rhsPeerId { + return false + } if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -144,6 +171,13 @@ enum GroupInfoEntry: PeerInfoEntry { } else if (lhsPeer != nil) != (rhsPeer != nil) { return false } + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if !lhsPresence.isEqual(to: rhsPresence) { + return false + } + } else if (lhsPresence != nil) != (rhsPresence != nil) { + return false + } return true } else { return false @@ -157,8 +191,13 @@ enum GroupInfoEntry: PeerInfoEntry { } } - var stableId: Int { - return self.sortIndex + var stableId: PeerInfoEntryStableId { + switch self { + case let .member(_, peerId, _, _, _): + return GroupPeerEntryStableId(peerId: peerId) + default: + return IntPeerInfoEntryStableId(value: self.sortIndex) + } } private var sortIndex: Int { @@ -179,7 +218,7 @@ enum GroupInfoEntry: PeerInfoEntry { return 6 case .addMember: return 7 - case let .member(index, _): + case let .member(index, _, _, _, _): return 10 + index case .leave: return 1000000 @@ -202,7 +241,14 @@ enum GroupInfoEntry: PeerInfoEntry { return PeerInfoActionItem(title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .blocks, action: { }) case let .notifications(settings): - return PeerInfoDisclosureItem(title: "Notifications", label: "Enabled", sectionId: self.section.rawValue, style: .blocks, action: { + let label: String + if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { + label = "Disabled" + } else { + label = "Enabled" + } + return PeerInfoDisclosureItem(title: "Notifications", label: label, sectionId: self.section.rawValue, style: .blocks, action: { + interaction.changeNotificationNoteSettings() }) case .sharedMedia: return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: self.section.rawValue, style: .blocks, action: { @@ -212,8 +258,15 @@ enum GroupInfoEntry: PeerInfoEntry { return PeerInfoPeerActionItem(icon: addMemberPlusIcon, title: "Add Member", sectionId: self.section.rawValue, action: { }) - case let .member(_, peer): - return PeerInfoPeerItem(account: account, peer: peer, sectionId: self.section.rawValue, action: { + case let .member(_, _, peer, presence, memberStatus): + let label: String? + switch memberStatus { + case .admin: + label = "admin" + case .member: + label = nil + } + return PeerInfoPeerItem(account: account, peer: peer, presence: presence, label: label, sectionId: self.section.rawValue, action: { if let peer = peer { interaction.openPeerInfo(peer.id) } @@ -230,15 +283,58 @@ enum GroupInfoEntry: PeerInfoEntry { func groupInfoEntries(view: PeerView) -> [PeerInfoEntry] { var entries: [PeerInfoEntry] = [] entries.append(GroupInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData)) - entries.append(GroupInfoEntry.setGroupPhoto) + + var highlightAdmins = false + var canManageGroup = false + if let group = view.peers[view.peerId] as? TelegramGroup { + if group.flags.contains(.adminsEnabled) { + highlightAdmins = true + switch group.role { + case .admin, .creator: + canManageGroup = true + case .member: + break + } + } else { + canManageGroup = true + } + } else if let channel = view.peers[view.peerId] as? TelegramChannel { + highlightAdmins = true + switch channel.role { + case .creator: + canManageGroup = true + case .editor, .moderator, .member: + break + } + } + + if canManageGroup { + entries.append(GroupInfoEntry.setGroupPhoto) + } entries.append(GroupInfoEntry.notifications(settings: view.notificationSettings)) entries.append(GroupInfoEntry.sharedMedia) - entries.append(GroupInfoEntry.addMember) + if canManageGroup { + entries.append(GroupInfoEntry.addMember) + } if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in + let lhsPresence = view.peerPresences[lhs.peerId] as? TelegramUserPresence + let rhsPresence = view.peerPresences[rhs.peerId] as? TelegramUserPresence + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if lhsPresence.status < rhsPresence.status { + return false + } else if lhsPresence.status > rhsPresence.status { + return true + } + } else if let _ = lhsPresence { + return true + } else if let _ = rhsPresence { + return false + } + switch lhs { case .creator: return false @@ -278,7 +374,18 @@ func groupInfoEntries(view: PeerView) -> [PeerInfoEntry] { for i in 0 ..< sortedParticipants.count { if let peer = view.peers[sortedParticipants[i].peerId] { - entries.append(GroupInfoEntry.member(index: i, peer: peer)) + let memberStatus: GroupInfoMemberStatus + if highlightAdmins { + switch sortedParticipants[i] { + case .admin, .creator: + memberStatus = .admin + case .member: + memberStatus = .member + } + } else { + memberStatus = .member + } + entries.append(GroupInfoEntry.member(index: i, peerId: peer.id, peer: peer, presence: view.peerPresences[peer.id], memberStatus: memberStatus)) } } } @@ -287,6 +394,10 @@ func groupInfoEntries(view: PeerView) -> [PeerInfoEntry] { if case .Member = group.membership { entries.append(GroupInfoEntry.leave) } + } else if let channel = view.peers[view.peerId] as? TelegramChannel { + if case .member = channel.participationStatus { + entries.append(GroupInfoEntry.leave) + } } return entries diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index fe537b2343..a064d5360f 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -108,11 +108,11 @@ private let titleFont = Font.medium(16.0) private let descriptionFont = Font.regular(13.0) private let extensionFont = Font.medium(13.0) -private let downloadFileStartIcon = generateTintedImage(image: UIImage(bundleImageName: "List Menu/ListDownloadStartIcon"), color: UIColor(0x1195f2)) +private let downloadFileStartIcon = generateTintedImage(image: UIImage(bundleImageName: "List Menu/ListDownloadStartIcon"), color: UIColor(0x007ee5)) private let downloadFilePauseIcon = generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0x1195f2).cgColor) + context.setFillColor(UIColor(0x007ee5).cgColor) context.fill(CGRect(x: 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0)) context.fill(CGRect(x: 2.0 + 2.0 + 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0)) @@ -177,7 +177,7 @@ final class ListMessageFileItemNode: ListMessageNode { self.downloadStatusIconNode.displayWithoutProcessing = true self.progressNode = ASDisplayNode() - self.progressNode.backgroundColor = UIColor(0x1195f2) + self.progressNode.backgroundColor = UIColor(0x007ee5) self.progressNode.isLayerBacked = true super.init() diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index caa97848a7..12050501c5 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -134,7 +134,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: UIColor.black)) } - mutableDescriptionText.append(NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: UIColor(0x1195f2))) + mutableDescriptionText.append(NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: UIColor(0x007ee5))) let style = NSMutableParagraphStyle() style.lineSpacing = 4.0 diff --git a/TelegramUI/PeerInfoActionItem.swift b/TelegramUI/PeerInfoActionItem.swift index e74426beb9..6d1cf01b9e 100644 --- a/TelegramUI/PeerInfoActionItem.swift +++ b/TelegramUI/PeerInfoActionItem.swift @@ -77,7 +77,7 @@ class PeerInfoActionItemNode: ListViewItemNode { private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode - let titleNode: TextNode + private let titleNode: TextNode init() { self.backgroundNode = ASDisplayNode() @@ -112,7 +112,7 @@ class PeerInfoActionItemNode: ListViewItemNode { return { item, width, neighbors in let sectionInset: CGFloat = 22.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.kind == .destructive ? UIColor(0xff3b30) : UIColor(0x1195f2)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.kind == .destructive ? UIColor(0xff3b30) : UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) let contentSize: CGSize let insets: UIEdgeInsets @@ -245,4 +245,18 @@ class PeerInfoActionItemNode: ListViewItemNode { } } } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.topStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.bottomStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.topStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.bottomStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + } } diff --git a/TelegramUI/PeerInfoAvatarAndNameItem.swift b/TelegramUI/PeerInfoAvatarAndNameItem.swift index f6303eee43..70767abe8f 100644 --- a/TelegramUI/PeerInfoAvatarAndNameItem.swift +++ b/TelegramUI/PeerInfoAvatarAndNameItem.swift @@ -113,7 +113,7 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { let statusColor: UIColor if let user = item.peer as? TelegramUser { statusText = "online" - statusColor = UIColor(0x1195f2) + statusColor = UIColor(0x007ee5) } else if let channel = item.peer as? TelegramChannel { if let cachedChannelData = item.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { statusText = "\(memberCount) members" @@ -163,8 +163,11 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { + let avatarOriginY: CGFloat switch item.style { case .plain: + avatarOriginY = 15.0 + if strongSelf.backgroundNode.supernode != nil { strongSelf.backgroundNode.removeFromSupernode() } @@ -175,6 +178,8 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { strongSelf.bottomStripeNode.removeFromSupernode() } case .blocks: + avatarOriginY = 13.0 + if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) } @@ -211,7 +216,7 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { strongSelf.avatarNode.setPeer(account: item.account, peer: peer) } - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 15.0, y: 15.0), size: CGSize(width: 66.0, height: 66.0)) + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 15.0, y: avatarOriginY), size: CGSize(width: 66.0, height: 66.0)) strongSelf.nameNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0), size: nameNodeLayout.size) strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0 + nameNodeLayout.size.height + 4.0), size: statusNodeLayout.size) diff --git a/TelegramUI/PeerInfoController.swift b/TelegramUI/PeerInfoController.swift index e9d6102abf..476dfaab09 100644 --- a/TelegramUI/PeerInfoController.swift +++ b/TelegramUI/PeerInfoController.swift @@ -16,11 +16,23 @@ final class PeerInfoControllerInteraction { } } +private struct PeerInfoSortableStableId: Hashable { + let id: PeerInfoEntryStableId + + static func ==(lhs: PeerInfoSortableStableId, rhs: PeerInfoSortableStableId) -> Bool { + return lhs.id.isEqual(to: rhs.id) + } + + var hashValue: Int { + return self.id.hashValue + } +} + private struct PeerInfoSortableEntry: Identifiable, Comparable { let entry: PeerInfoEntry - var stableId: Int { - return self.entry.stableId + var stableId: PeerInfoSortableStableId { + return PeerInfoSortableStableId(id: self.entry.stableId) } static func ==(lhs: PeerInfoSortableEntry, rhs: PeerInfoSortableEntry) -> Bool { @@ -48,6 +60,12 @@ private func preparedPeerInfoEntryTransition(account: Account, from fromEntries: return PeerInfoEntryTransition(deletions: deletions, insertions: insertions, updates: updates) } +private struct PeerInfoEquatableState: Equatable { + static func ==(lhs: PeerInfoEquatableState, rhs: PeerInfoEquatableState) -> Bool { + + } +} + public final class PeerInfoController: ListController { private let account: Account private let peerId: PeerId @@ -62,6 +80,7 @@ public final class PeerInfoController: ListController { private let changeSettingsDisposable = MetaDisposable() private var currentListStyle: PeerInfoListStyle = .plain + private var state = Promise(nil) public init(account: Account, peerId: PeerId) { self.account = account @@ -156,6 +175,8 @@ public final class PeerInfoController: ListController { let style: PeerInfoListStyle if let group = view.peers[view.peerId] as? TelegramGroup { style = .blocks + } else if let channel = view.peers[view.peerId] as? TelegramChannel, case .group = channel.info { + style = .blocks } else { style = .plain } diff --git a/TelegramUI/PeerInfoDisclosureItem.swift b/TelegramUI/PeerInfoDisclosureItem.swift index 8ce6b242cb..7496b8222b 100644 --- a/TelegramUI/PeerInfoDisclosureItem.swift +++ b/TelegramUI/PeerInfoDisclosureItem.swift @@ -242,4 +242,20 @@ class PeerInfoDisclosureItemNode: ListViewItemNode { } } } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.topStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.bottomStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.topStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.bottomStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + } } diff --git a/TelegramUI/PeerInfoEntries.swift b/TelegramUI/PeerInfoEntries.swift index b3ab33a421..c2a1bc4749 100644 --- a/TelegramUI/PeerInfoEntries.swift +++ b/TelegramUI/PeerInfoEntries.swift @@ -11,16 +11,45 @@ protocol PeerInfoSection { protocol PeerInfoEntryStableId { func isEqual(to: PeerInfoEntryStableId) -> Bool + var hashValue: Int { get } +} + +struct IntPeerInfoEntryStableId: PeerInfoEntryStableId { + let value: Int + + func isEqual(to: PeerInfoEntryStableId) -> Bool { + if let to = to as? IntPeerInfoEntryStableId, to.value == self.value { + return true + } else { + return false + } + } + + var hashValue: Int { + return self.value.hashValue + } } protocol PeerInfoEntry { var section: PeerInfoSection { get } - var stableId: Int { get } + var stableId: PeerInfoEntryStableId { get } func isEqual(to: PeerInfoEntry) -> Bool func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem } +enum PeerInfoNavigationButton { + case none + case edit + case done +} + +protocol PeerInfoState { + func isEqual(to: PeerInfoState) -> Bool + + var navigationButton: PeerInfoNavigationButton { get } +} + func peerInfoEntries(view: PeerView) -> [PeerInfoEntry] { if let user = view.peers[view.peerId] as? TelegramUser { return userInfoEntries(view: view) @@ -29,7 +58,7 @@ func peerInfoEntries(view: PeerView) -> [PeerInfoEntry] { case .broadcast: return channelBroadcastInfoEntries(view: view) case .group: - return [] + return groupInfoEntries(view: view) } } else if let group = view.peers[view.peerId] as? TelegramGroup { return groupInfoEntries(view: view) diff --git a/TelegramUI/PeerInfoPeerActionItem.swift b/TelegramUI/PeerInfoPeerActionItem.swift index e71ddfab2e..e539a7e2f0 100644 --- a/TelegramUI/PeerInfoPeerActionItem.swift +++ b/TelegramUI/PeerInfoPeerActionItem.swift @@ -105,7 +105,7 @@ class PeerInfoPeerActionItemNode: ListViewItemNode { return { item, width, neighbors in let leftInset: CGFloat = 65.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x1195f2)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) let contentSize: CGSize let insets: UIEdgeInsets @@ -204,4 +204,20 @@ class PeerInfoPeerActionItemNode: ListViewItemNode { } } } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.topStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.bottomStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.topStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.bottomStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + } } diff --git a/TelegramUI/PeerInfoPeerItem.swift b/TelegramUI/PeerInfoPeerItem.swift index 3daaa2fbe5..3c349e44b1 100644 --- a/TelegramUI/PeerInfoPeerItem.swift +++ b/TelegramUI/PeerInfoPeerItem.swift @@ -5,15 +5,19 @@ import SwiftSignalKit import Postbox import TelegramCore -class PeerInfoPeerItem: ListViewItem, PeerInfoItem { +final class PeerInfoPeerItem: ListViewItem, PeerInfoItem { let account: Account let peer: Peer? + let presence: PeerPresence? + let label: String? let sectionId: PeerInfoItemSectionId let action: () -> Void - init(account: Account, peer: Peer?, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) { + init(account: Account, peer: Peer?, presence: PeerPresence?, label: String?, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) { self.account = account self.peer = peer + self.presence = presence + self.label = label self.sectionId = sectionId self.action = action } @@ -60,6 +64,7 @@ class PeerInfoPeerItem: ListViewItem, PeerInfoItem { private let titleFont = Font.regular(17.0) private let titleBoldFont = Font.medium(17.0) private let statusFont = Font.regular(14.0) +private let labelFont = Font.regular(13.0) private let avatarFont = Font.regular(17.0) class PeerInfoPeerItemNode: ListViewItemNode { @@ -70,8 +75,12 @@ class PeerInfoPeerItemNode: ListViewItemNode { private let avatarNode: AvatarNode private let titleNode: TextNode + private let labelNode: TextNode private let statusNode: TextNode + private var peerPresenceManager: PeerPresenceStatusManager? + private var layoutParams: (PeerInfoPeerItem, CGFloat, PeerInfoItemNeighbors)? + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -98,6 +107,11 @@ class PeerInfoPeerItemNode: ListViewItemNode { self.statusNode.contentMode = .left self.statusNode.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 @@ -107,15 +121,25 @@ class PeerInfoPeerItemNode: ListViewItemNode { self.addSubnode(self.avatarNode) self.addSubnode(self.titleNode) self.addSubnode(self.statusNode) + self.addSubnode(self.labelNode) + + self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in + if let strongSelf = self, let layoutParams = strongSelf.layoutParams { + let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.1, layoutParams.2) + apply() + } + }) } func asyncLayout() -> (_ item: PeerInfoPeerItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) return { item, width, neighbors in var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? + var labelAttributedString: NSAttributedString? if let peer = item.peer { if let user = peer as? TelegramUser { @@ -133,7 +157,13 @@ class PeerInfoPeerItemNode: ListViewItemNode { titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6)) } - statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6)) + if let presence = item.presence as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? UIColor(0x007ee5) : UIColor(0xa6a6a6)) + } else { + statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6)) + } } else if let group = peer as? TelegramGroup { titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) } else if let channel = peer as? TelegramChannel { @@ -141,10 +171,16 @@ class PeerInfoPeerItemNode: ListViewItemNode { } } + if let label = item.label { + labelAttributedString = NSAttributedString(string: label, font: labelFont, textColor: UIColor(0xa6a6a6)) + } + let leftInset: CGFloat = 65.0 - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil) - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil) + let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil) + + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - labelLayout.size.width, height: CGFloat.greatestFiniteMagnitude), nil) + let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - (labelLayout.size.width > 0.0 ? (labelLayout.size.width) + 15.0 : 0.0), height: CGFloat.greatestFiniteMagnitude), nil) let contentSize: CGSize let insets: UIEdgeInsets @@ -172,8 +208,13 @@ class PeerInfoPeerItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { + strongSelf.layoutParams = (item, width, neighbors) + let _ = titleApply() let _ = statusApply() + let _ = labelApply() + + strongSelf.labelNode.isHidden = labelAttributedString == nil if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) @@ -203,13 +244,18 @@ class PeerInfoPeerItemNode: ListViewItemNode { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 5.0), size: titleLayout.size) strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 25.0), size: statusLayout.size) + strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: width - labelLayout.size.width - 15.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0 - labelLayout.size.height / 10.0)), size: labelLayout.size) - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)) + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)) if let peer = item.peer { strongSelf.avatarNode.setPeer(account: item.account, peer: peer) } strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 48.0 + UIScreenPixel + UIScreenPixel)) + + if let presence = item.presence as? TelegramUserPresence { + strongSelf.peerPresenceManager?.reset(presence: presence) + } } }) } @@ -252,4 +298,24 @@ class PeerInfoPeerItemNode: ListViewItemNode { } } } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.topStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.bottomStripeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.topStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.bottomStripeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.titleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.avatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + } } diff --git a/TelegramUI/PeerInfoTextWithLabelItem.swift b/TelegramUI/PeerInfoTextWithLabelItem.swift index 834bab29cc..099e1e8edf 100644 --- a/TelegramUI/PeerInfoTextWithLabelItem.swift +++ b/TelegramUI/PeerInfoTextWithLabelItem.swift @@ -93,7 +93,7 @@ class PeerInfoTextWithLabelItemNode: ListViewItemNode { let insets = peerInfoItemNeighborsPlainInsets(neighbors) let leftInset: CGFloat = 35.0 - let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: labelFont, textColor: UIColor(0x1195f2)), nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil) + let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: labelFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil) let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.text, font: textFont, textColor: UIColor.black), nil, item.multiline ? 0 : 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), nil) let contentSize = CGSize(width: width, height: textLayout.size.height + 39.0) return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in @@ -115,4 +115,10 @@ class PeerInfoTextWithLabelItemNode: ListViewItemNode { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) self.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + } } diff --git a/TelegramUI/PeerMediaCollectionModeSelectionNode.swift b/TelegramUI/PeerMediaCollectionModeSelectionNode.swift index 417b651170..ab2d09c403 100644 --- a/TelegramUI/PeerMediaCollectionModeSelectionNode.swift +++ b/TelegramUI/PeerMediaCollectionModeSelectionNode.swift @@ -2,7 +2,7 @@ import Foundation import AsyncDisplayKit import Display -private let checkmarkImage = generateTintedImage(image: UIImage(bundleImageName: "List Menu/Checkmark")?.precomposed(), color: UIColor(0x1195f2)) +private let checkmarkImage = generateTintedImage(image: UIImage(bundleImageName: "List Menu/Checkmark")?.precomposed(), color: UIColor(0x007ee5)) private final class PeerMediaCollectionModeSelectionCaseNode: ASDisplayNode { fileprivate let mode: PeerMediaCollectionMode @@ -17,7 +17,7 @@ private final class PeerMediaCollectionModeSelectionCaseNode: ASDisplayNode { var isSelected = false { didSet { if self.isSelected != oldValue { - self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(self.mode), font: Font.regular(17.0), textColor: isSelected ? UIColor(0x1195f2) : UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(self.mode), font: Font.regular(17.0), textColor: isSelected ? UIColor(0x007ee5) : UIColor.black) self.checkmarkView.isHidden = !self.isSelected } } diff --git a/TelegramUI/PeerPresenceStatusManager.swift b/TelegramUI/PeerPresenceStatusManager.swift new file mode 100644 index 0000000000..d8635a9b2e --- /dev/null +++ b/TelegramUI/PeerPresenceStatusManager.swift @@ -0,0 +1,32 @@ +import Foundation +import SwiftSignalKit +import TelegramCore + +final class PeerPresenceStatusManager { + private let update: () -> Void + private var timer: SwiftSignalKit.Timer? + + init(update: @escaping () -> Void) { + self.update = update + } + + deinit { + self.timer?.invalidate() + } + + func reset(presence: TelegramUserPresence) { + timer?.invalidate() + timer = nil + + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let timeout = userPresenceStringRefreshTimeout(presence, relativeTo: Int32(timestamp)) + if timeout.isFinite { + self.timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.update() + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } + } +} diff --git a/TelegramUI/PresenceStrings.swift b/TelegramUI/PresenceStrings.swift new file mode 100644 index 0000000000..3584dec001 --- /dev/null +++ b/TelegramUI/PresenceStrings.swift @@ -0,0 +1,140 @@ +import Foundation +import Postbox +import TelegramCore + +func stringForTimestamp(day: Int32, month: Int32, year: Int32) -> String { + return String(format: "%d.%02d.%02d", day, month, year - 100) +} + +func stringForTime(hours: Int32, minutes: Int32) -> String { + return String(format: "%d:%02d", hours, minutes) +} + +enum UserPresenceDay { + case today + case yesterday +} + +func stringForUserPresence(day: UserPresenceDay, hours: Int32, minutes: Int32) -> String { + let dayString: String + switch day { + case .today: + dayString = "today" + case .yesterday: + dayString = "yesterday" + } + return "last seen \(dayString) at \(stringForTime(hours: hours, minutes: minutes))" +} + +enum RelativeUserPresenceLastSeen { + case justNow + case minutesAgo(Int32) + case hoursAgo(Int32) + case todayAt(hours: Int32, minutes: Int32) + case yesterdayAt(hours: Int32, minutes: Int32) + case thisYear(month: Int32, day: Int32) + case atDate(year: Int32, month: Int32) +} + +enum RelativeUserPresenceStatus { + case offline + case online(at: Int32) + case lastSeen(at: Int32) + case recently + case lastWeek + case lastMonth +} + +func relativeUserPresenceStatus(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> RelativeUserPresenceStatus { + switch presence.status { + case .none: + return .offline + case let .present(statusTimestamp): + if statusTimestamp >= timestamp { + return .online(at: statusTimestamp) + } else { + return .lastSeen(at: statusTimestamp) + } + case .recently: + return .recently + case .lastWeek: + return .lastWeek + case .lastMonth: + return .lastMonth + } +} + +func stringAndActivityForUserPresence(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> (String, Bool) { + switch presence.status { + case .none: + return ("offline", false) + case let .present(statusTimestamp): + if statusTimestamp >= timestamp { + return ("online", true) + } else { + let difference = timestamp - statusTimestamp + if difference < 30 { + return ("last seen just now", false) + } else if difference < 60 * 60 { + let minutes = difference / 60 + if minutes <= 1 { + return ("last seen 1 minute ago", false) + } else { + return ("last seen \(minutes) minutes ago", false) + } + } else { + var t: time_t = time_t(statusTimestamp) + 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 ("last seen \(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))", false) + } + + let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday + if dayDifference == 0 || dayDifference == -1 { + let day: UserPresenceDay + if dayDifference == 0 { + day = .today + } else { + day = .yesterday + } + return (stringForUserPresence(day: day, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min), false) + } else { + return ("last seen \(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))", false) + } + } + } + case .recently: + return ("last seen recently", false) + case .lastWeek: + return ("last seen last week", false) + case .lastMonth: + return ("last seen last month", false) + } +} + +func userPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> Double { + switch presence.status { + case let .present(statusTimestamp): + if statusTimestamp >= timestamp { + return Double(statusTimestamp - timestamp) + } else { + let difference = timestamp - statusTimestamp + if difference < 30 { + return Double((30 - difference) + 1) + } else if difference < 60 * 60 { + return Double((difference % 60) + 1) + } else { + return Double.infinity + } + return Double.infinity + } + case .recently, .none, .lastWeek, .lastMonth: + return Double.infinity + } +} diff --git a/TelegramUI/ReplyAccessoryPanelNode.swift b/TelegramUI/ReplyAccessoryPanelNode.swift index f2b1ddce1f..9cf1e327fa 100644 --- a/TelegramUI/ReplyAccessoryPanelNode.swift +++ b/TelegramUI/ReplyAccessoryPanelNode.swift @@ -23,7 +23,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.closeButton.displaysAsynchronously = false self.lineNode = ASDisplayNode() - self.lineNode.backgroundColor = UIColor(0x1195f2) + self.lineNode.backgroundColor = UIColor(0x007ee5) self.titleNode = ASTextNode() self.titleNode.truncationMode = .byTruncatingTail @@ -56,7 +56,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { text = messageText } - strongSelf.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.regular(14.5), textColor: UIColor(0x1195f2)) + strongSelf.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.regular(14.5), textColor: UIColor(0x007ee5)) strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.5), textColor: UIColor.black) strongSelf.setNeedsLayout() diff --git a/TelegramUI/SearchBarNode.swift b/TelegramUI/SearchBarNode.swift index bcb1f3947a..aad36d25b3 100644 --- a/TelegramUI/SearchBarNode.swift +++ b/TelegramUI/SearchBarNode.swift @@ -95,7 +95,7 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.cancelButton = ASButtonNode() self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) - self.cancelButton.setAttributedTitle(NSAttributedString(string: "Cancel", font: Font.regular(17.0), textColor: UIColor(0x1195f2)), for: []) + self.cancelButton.setAttributedTitle(NSAttributedString(string: "Cancel", font: Font.regular(17.0), textColor: UIColor(0x007ee5)), for: []) self.cancelButton.displaysAsynchronously = false super.init() diff --git a/TelegramUI/SettingsAccountInfoItem.swift b/TelegramUI/SettingsAccountInfoItem.swift index 208f0fda99..ab8dd4c9f9 100644 --- a/TelegramUI/SettingsAccountInfoItem.swift +++ b/TelegramUI/SettingsAccountInfoItem.swift @@ -78,7 +78,7 @@ class SettingsAccountInfoItemNode: ListControllerGroupableItemNode { statusColor = UIColor(0xb3b3b3) case .Online: statusText = "online" - statusColor = UIColor(0x1195f2) + statusColor = UIColor(0x007ee5) } let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index 1596f45aa2..3fb80573e7 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -258,6 +258,7 @@ final class TextNode: ASDisplayNode { var updated = false if let existingLayout = existingLayout, existingLayout.constrainedSize == constrainedSize && existingLayout.maximumNumberOfLines == maximumNumberOfLines && existingLayout.truncationType == truncationType && existingLayout.cutout == cutout { + let stringMatch: Bool if let existingString = existingLayout.attributedString, let string = attributedString { stringMatch = existingString.isEqual(to: string) diff --git a/TelegramUI/UserInfoEntries.swift b/TelegramUI/UserInfoEntries.swift index 6af3c984a9..e8fd1a7de5 100644 --- a/TelegramUI/UserInfoEntries.swift +++ b/TelegramUI/UserInfoEntries.swift @@ -50,8 +50,8 @@ enum UserInfoEntry: PeerInfoEntry { } } - var stableId: Int { - return self.sortIndex + var stableId: PeerInfoEntryStableId { + return IntPeerInfoEntryStableId(value: self.sortIndex) } func isEqual(to: PeerInfoEntry) -> Bool { @@ -229,7 +229,31 @@ enum UserInfoEntry: PeerInfoEntry { } } -func userInfoEntries(view: PeerView) -> [PeerInfoEntry] { +final class UserInfoEditingState { + +} + +final class UserInfoState: PeerInfoState { + fileprivate let editingState: UserInfoEditingState? + + var navigationButton: PeerInfoNavigationButton { + return self.editingState == nil ? .edit : .done + } + + init() { + self.editingState = nil + } + + func isEqual(to: PeerInfoState) -> Bool { + if let to = to as? UserInfoState { + return true + } else { + return false + } + } +} + +func userInfoEntries(view: PeerView, state: PeerInfoState?) -> [PeerInfoEntry] { var entries: [PeerInfoEntry] = [] entries.append(UserInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData)) if let cachedUserData = view.cachedData as? CachedUserData { @@ -244,9 +268,13 @@ func userInfoEntries(view: PeerView) -> [PeerInfoEntry] { if let username = user.username, !username.isEmpty { entries.append(UserInfoEntry.userName(value: username)) } - entries.append(UserInfoEntry.sendMessage) - entries.append(UserInfoEntry.shareContact) - entries.append(UserInfoEntry.startSecretChat) + if let state = state as? UserInfoState, let editingState = state.editingState { + + } else { + entries.append(UserInfoEntry.sendMessage) + entries.append(UserInfoEntry.shareContact) + entries.append(UserInfoEntry.startSecretChat) + } entries.append(UserInfoEntry.sharedMedia) entries.append(UserInfoEntry.notifications(settings: view.notificationSettings)) entries.append(UserInfoEntry.block)