diff --git a/Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/Contents.json b/Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/Contents.json new file mode 100644 index 0000000000..0928a8ec6a --- /dev/null +++ b/Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "GroupInfoIconAddMember@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "GroupInfoIconAddMember@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@2x.png b/Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@2x.png new file mode 100644 index 0000000000..8a96306a36 Binary files /dev/null and b/Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@2x.png differ diff --git a/Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@3x.png b/Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@3x.png new file mode 100644 index 0000000000..d459a05545 Binary files /dev/null and b/Images.xcassets/Peer Info/PeerItemPlusIcon.imageset/GroupInfoIconAddMember@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 122fb4c348..1e587526a0 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -32,8 +32,14 @@ D0B417C31D7DE54E004562A4 /* ChatPresentationInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */; }; D0B7F8E21D8A18070045D939 /* PeerMediaCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B7F8E11D8A18070045D939 /* PeerMediaCollectionController.swift */; }; D0B7F8E81D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B7F8E71D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift */; }; - D0B843901DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8438F1DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift */; }; D0B843921DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843911DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift */; }; + D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */; }; + D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */; }; + D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */; }; + D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */; }; + 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 */; }; 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 */; }; @@ -65,7 +71,6 @@ D0E7A1BF1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7A1BE1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift */; }; D0E7A1C11D8C258D00C37A6F /* ChatHistoryEntriesForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7A1C01D8C258D00C37A6F /* ChatHistoryEntriesForView.swift */; }; D0E7A1C31D8C25D600C37A6F /* PreparedChatHistoryViewTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7A1C21D8C25D600C37A6F /* PreparedChatHistoryViewTransition.swift */; }; - D0EE97161D88BB53006C18E1 /* UserInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EE97151D88BB53006C18E1 /* UserInfoController.swift */; }; D0EE971A1D88BCA0006C18E1 /* ChatInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EE97191D88BCA0006C18E1 /* ChatInfo.swift */; }; D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */; }; D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */; }; @@ -239,8 +244,14 @@ D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPresentationInterfaceState.swift; sourceTree = ""; }; D0B7F8E11D8A18070045D939 /* PeerMediaCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionController.swift; sourceTree = ""; }; D0B7F8E71D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionControllerNode.swift; sourceTree = ""; }; - D0B8438F1DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBroadcastInfoController.swift; sourceTree = ""; }; D0B843911DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoDisclosureItem.swift; sourceTree = ""; }; + D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoController.swift; sourceTree = ""; }; + D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoEntries.swift; sourceTree = ""; }; + D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoEntries.swift; sourceTree = ""; }; + D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoEntries.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -272,7 +283,6 @@ D0E7A1BE1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryViewForLocation.swift; sourceTree = ""; }; D0E7A1C01D8C258D00C37A6F /* ChatHistoryEntriesForView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryEntriesForView.swift; sourceTree = ""; }; D0E7A1C21D8C25D600C37A6F /* PreparedChatHistoryViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreparedChatHistoryViewTransition.swift; sourceTree = ""; }; - D0EE97151D88BB53006C18E1 /* UserInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoController.swift; sourceTree = ""; }; D0EE97191D88BCA0006C18E1 /* ChatInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInfo.swift; sourceTree = ""; }; D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaFrameSourceContext.swift; sourceTree = ""; }; D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerAudioRenderer.swift; sourceTree = ""; }; @@ -450,6 +460,8 @@ D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */, D03120F51DA534C1006A2A60 /* PeerInfoActionItem.swift */, D0B843911DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift */, + D0B843D81DAAAA0C005F29E1 /* PeerInfoPeerItem.swift */, + D0B843DA1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift */, ); name = Components; sourceTree = ""; @@ -633,18 +645,21 @@ isa = PBXGroup; children = ( D003702C1DA43006004308D3 /* Components */, - D0EE97141D88BB39006C18E1 /* User */, + D0EE97141D88BB39006C18E1 /* Controller */, ); name = "Peer Info"; sourceTree = ""; }; - D0EE97141D88BB39006C18E1 /* User */ = { + D0EE97141D88BB39006C18E1 /* Controller */ = { isa = PBXGroup; children = ( - D0EE97151D88BB53006C18E1 /* UserInfoController.swift */, - D0B8438F1DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift */, + D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */, + D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */, + D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */, + D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */, + D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */, ); - name = User; + name = Controller; sourceTree = ""; }; D0F69CCE1D6B87950046BCD6 /* Files */ = { @@ -1190,6 +1205,7 @@ D0F69E661D6B8BF90046BCD6 /* ZoomableContentGalleryItemNode.swift in Sources */, D0DE77001D92F1EB002B8809 /* ChatTitleView.swift in Sources */, D0F69DF01D6B8A6C0046BCD6 /* AuthorizationCodeControllerNode.swift in Sources */, + D0B843DB1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift in Sources */, D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */, D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */, D0F69D521D6B87D30046BCD6 /* MediaPlayer.swift in Sources */, @@ -1219,6 +1235,7 @@ D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */, D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */, D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */, + D0B843CD1DA903BB005F29E1 /* PeerInfoController.swift in Sources */, D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */, D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */, D0B843921DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift in Sources */, @@ -1228,6 +1245,7 @@ D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */, D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */, D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */, + D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */, D0DF0CA61D82BCE0008AEB01 /* MentionsTableCell.swift in Sources */, D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */, D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */, @@ -1284,7 +1302,7 @@ D0F69E7D1D6B8C470046BCD6 /* SettingsController.swift in Sources */, D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */, D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, - D0EE97161D88BB53006C18E1 /* UserInfoController.swift in Sources */, + D0B843D91DAAAA0C005F29E1 /* PeerInfoPeerItem.swift in Sources */, D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */, D0DE77271D932627002B8809 /* ChatHistoryNode.swift in Sources */, D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */, @@ -1299,8 +1317,11 @@ D0E7A1BF1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift in Sources */, D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */, D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */, + D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */, + D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */, D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */, D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */, + D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */, D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */, D0F69DE11D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift in Sources */, D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */, @@ -1324,7 +1345,6 @@ D0F69DE21D6B8A420046BCD6 /* ListControllerGroupableItem.swift in Sources */, D0F69D791D6B87DF0046BCD6 /* MediaTrackFrame.swift in Sources */, D0F69DC91D6B89EB0046BCD6 /* ImageNode.swift in Sources */, - D0B843901DA7E63E005F29E1 /* ChannelBroadcastInfoController.swift in Sources */, D0DE77251D93225E002B8809 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */, D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */, D0F69D311D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift in Sources */, diff --git a/TelegramUI/ChannelBroadcastInfoController.swift b/TelegramUI/ChannelBroadcastInfoController.swift deleted file mode 100644 index 4d2018c473..0000000000 --- a/TelegramUI/ChannelBroadcastInfoController.swift +++ /dev/null @@ -1,333 +0,0 @@ -import Foundation -import Display -import Postbox -import SwiftSignalKit -import TelegramCore - -private enum ChannelInfoSection: UInt32 { - case info - case sharedMediaAndNotifications - case reportOrLeave -} - -private enum ChannelInfoEntry: Comparable, Identifiable { - case info(peer: Peer?, cachedData: CachedPeerData?) - case about(text: String) - case userName(value: String) - case sharedMedia - case notifications(settings: PeerNotificationSettings?) - case report - case leave - - fileprivate var section: ChannelInfoSection { - switch self { - case .info, .about, .userName: - return .info - case .sharedMedia, .notifications: - return .sharedMediaAndNotifications - case .report, .leave: - return .reportOrLeave - } - } - - fileprivate var stableId: Int { - return self.sortIndex - } - - fileprivate static func ==(lhs: ChannelInfoEntry, rhs: ChannelInfoEntry) -> Bool { - switch lhs { - case let .info(lhsPeer, lhsCachedData): - switch rhs { - case let .info(rhsPeer, rhsCachedData): - if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { - if !lhsPeer.isEqual(rhsPeer) { - return false - } - } else if (lhsPeer == nil) != (rhsPeer != nil) { - return false - } - if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { - if !lhsCachedData.isEqual(to: rhsCachedData) { - return false - } - } else if (rhsCachedData == nil) != (rhsCachedData != nil) { - return false - } - return true - default: - return false - } - case let .about(lhsText): - switch rhs { - case let .about(lhsText): - return true - default: - return false - } - case let .userName(value): - switch rhs { - case .userName(value): - return true - default: - return false - } - case .sharedMedia: - switch rhs { - case .sharedMedia: - return true - default: - return false - } - case let .notifications(lhsSettings): - switch rhs { - case let .notifications(rhsSettings): - if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { - return lhsSettings.isEqual(to: rhsSettings) - } else if (lhsSettings != nil) != (rhsSettings != nil) { - return false - } - return true - default: - return false - } - case .report: - switch rhs { - case .report: - return true - default: - return false - } - case .leave: - switch rhs { - case .leave: - return true - default: - return false - } - } - } - - private var sortIndex: Int { - switch self { - case .info: - return 0 - case .about: - return 1 - case .userName: - return 1000 - case .sharedMedia: - return 1004 - case .notifications: - return 1005 - case .report: - return 1006 - case .leave: - return 1007 - } - } - - fileprivate static func <(lhs: ChannelInfoEntry, rhs: ChannelInfoEntry) -> Bool { - return lhs.sortIndex < rhs.sortIndex - } -} - -private func channelBroadcastInfoEntries(account: Account, peerId: PeerId) -> Signal<[ChannelInfoEntry], NoError> { - return account.viewTracker.peerView(peerId) - |> map { view -> [ChannelInfoEntry] in - var entries: [ChannelInfoEntry] = [] - entries.append(.info(peer: view.peers[peerId], cachedData: view.cachedData)) - if let cachedChannelData = view.cachedData as? CachedChannelData { - if let about = cachedChannelData.about, !about.isEmpty { - entries.append(.about(text: about)) - } - } - if let channel = view.peers[peerId] as? TelegramChannel { - if let username = channel.username, !username.isEmpty { - entries.append(.userName(value: username)) - } - entries.append(.sharedMedia) - entries.append(.notifications(settings: view.notificationSettings)) - entries.append(.report) - if channel.participationStatus == .member { - entries.append(.leave) - } - } - return entries - } -} - -private struct ChannelInfoEntryTransition { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] -} - -private func infoItemForEntry(account: Account, entry: ChannelInfoEntry, interaction: PeerInfoControllerInteraction) -> ListViewItem { - switch entry { - case let .info(peer, cachedData): - return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: entry.section.rawValue) - case let .about(text): - return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: entry.section.rawValue) - case let .userName(value): - return PeerInfoTextWithLabelItem(label: "share link", text: "https://telegram.me/\(value)", multiline: false, sectionId: entry.section.rawValue) - return PeerInfoActionItem(title: "Start Secret Chat", kind: .generic, sectionId: entry.section.rawValue, action: { - - }) - case .sharedMedia: - return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: entry.section.rawValue, action: { - interaction.openSharedMedia() - }) - case let .notifications(settings): - 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: entry.section.rawValue, action: { - interaction.changeNotificationNoteSettings() - }) - case .report: - return PeerInfoActionItem(title: "Report", kind: .generic, sectionId: entry.section.rawValue, action: { - - }) - case .leave: - return PeerInfoActionItem(title: "Leave Channel", kind: .destructive, sectionId: entry.section.rawValue, action: { - - }) - } -} - -private func preparedUserInfoEntryTransition(account: Account, from fromEntries: [ChannelInfoEntry], to toEntries: [ChannelInfoEntry], interaction: PeerInfoControllerInteraction) -> ChannelInfoEntryTransition { - 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: infoItemForEntry(account: account, entry: $0.1, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: infoItemForEntry(account: account, entry: $0.1, interaction: interaction), directionHint: nil) } - - return ChannelInfoEntryTransition(deletions: deletions, insertions: insertions, updates: updates) -} - -public class ChannelBroadcastInfoController: ListController { - private let account: Account - private let peerId: PeerId - - private var _ready = Promise() - override public var ready: Promise { - return self._ready - } - private var didSetReady = false - - private let transitionDisposable = MetaDisposable() - private let changeSettingsDisposable = MetaDisposable() - - public init(account: Account, peerId: PeerId) { - self.account = account - self.peerId = peerId - - super.init() - - self.title = "Info" - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.transitionDisposable.dispose() - self.changeSettingsDisposable.dispose() - } - - override public func displayNodeDidLoad() { - super.displayNodeDidLoad() - - let interaction = PeerInfoControllerInteraction(openSharedMedia: { [weak self] in - if let strongSelf = self { - if let controller = peerSharedMediaController(account: strongSelf.account, peerId: strongSelf.peerId) { - (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) - } - } - }, changeNotificationNoteSettings: { [weak self] in - if let strongSelf = self { - let controller = ActionSheetController() - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - let notificationAction: (Int32) -> Void = { [weak strongSelf] muteUntil in - if let strongSelf = strongSelf { - let muteState: PeerMuteState - if muteUntil <= 0 { - muteState = .unmuted - } else if muteUntil == Int32.max { - muteState = .muted(until: Int32.max) - } else { - muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) - } - strongSelf.changeSettingsDisposable.set(changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.appDefault)).start()) - } - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Enable", action: { - dismissAction() - notificationAction(0) - }), - ActionSheetButtonItem(title: "Mute for 1 hour", action: { - dismissAction() - notificationAction(1 * 60 * 60) - }), - ActionSheetButtonItem(title: "Mute for 8 hours", action: { - dismissAction() - notificationAction(8 * 60 * 60) - }), - ActionSheetButtonItem(title: "Mute for 2 days", action: { - dismissAction() - notificationAction(2 * 24 * 60 * 60) - }), - ActionSheetButtonItem(title: "Disable", action: { - dismissAction() - notificationAction(Int32.max) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) - ]) - strongSelf.present(controller, in: .window) - } - }) - - self.listDisplayNode.backgroundColor = UIColor.white - - let previousEntries = Atomic<[ChannelInfoEntry]?>(value: nil) - - let account = self.account - let transition = channelBroadcastInfoEntries(account: self.account, peerId: self.peerId) - |> map { entries -> (ChannelInfoEntryTransition, Bool, Bool) in - let previous = previousEntries.swap(entries) - return (preparedUserInfoEntryTransition(account: account, 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) - })) - } - - private func enqueueTransition(_ transition: ChannelInfoEntryTransition, firstTime: Bool, animated: Bool) { - var options = ListViewDeleteAndInsertOptions() - if firstTime { - options.insert(.Synchronous) - options.insert(.LowLatency) - } else if animated { - options.insert(.AnimateInsertion) - } - self.listDisplayNode.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)) - } - } - }) - } -} diff --git a/TelegramUI/ChannelInfoEntries.swift b/TelegramUI/ChannelInfoEntries.swift new file mode 100644 index 0000000000..69d75cc085 --- /dev/null +++ b/TelegramUI/ChannelInfoEntries.swift @@ -0,0 +1,210 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +private enum ChannelInfoSection: UInt32, PeerInfoSection { + case info + case sharedMediaAndNotifications + case reportOrLeave + + func isEqual(to: PeerInfoSection) -> Bool { + guard let section = to as? ChannelInfoSection else { + return false + } + return section == self + } + + func isOrderedBefore(_ section: PeerInfoSection) -> Bool { + guard let section = section as? ChannelInfoSection else { + return false + } + return self.rawValue < section.rawValue + } +} + +enum ChannelInfoEntry: PeerInfoEntry { + case info(peer: Peer?, cachedData: CachedPeerData?) + case about(text: String) + case userName(value: String) + case sharedMedia + case notifications(settings: PeerNotificationSettings?) + case report + case leave + + var section: PeerInfoSection { + switch self { + case .info, .about, .userName: + return ChannelInfoSection.info + case .sharedMedia, .notifications: + return ChannelInfoSection.sharedMediaAndNotifications + case .report, .leave: + return ChannelInfoSection.reportOrLeave + } + } + + var stableId: Int { + return self.sortIndex + } + + func isEqual(to: PeerInfoEntry) -> Bool { + guard let entry = to as? ChannelInfoEntry else { + return false + } + switch self { + case let .info(lhsPeer, lhsCachedData): + switch entry { + case let .info(rhsPeer, rhsCachedData): + if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhsPeer == nil) != (rhsPeer != nil) { + return false + } + if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { + if !lhsCachedData.isEqual(to: rhsCachedData) { + return false + } + } else if (rhsCachedData == nil) != (rhsCachedData != nil) { + return false + } + return true + default: + return false + } + case let .about(lhsText): + switch entry { + case let .about(lhsText): + return true + default: + return false + } + case let .userName(value): + switch entry { + case .userName(value): + return true + default: + return false + } + case .sharedMedia: + switch entry { + case .sharedMedia: + return true + default: + return false + } + case let .notifications(lhsSettings): + switch entry { + case let .notifications(rhsSettings): + if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { + return lhsSettings.isEqual(to: rhsSettings) + } else if (lhsSettings != nil) != (rhsSettings != nil) { + return false + } + return true + default: + return false + } + case .report: + switch entry { + case .report: + return true + default: + return false + } + case .leave: + switch entry { + case .leave: + return true + default: + return false + } + } + } + + private var sortIndex: Int { + switch self { + case .info: + return 0 + case .about: + return 1 + case .userName: + return 1000 + case .sharedMedia: + return 1004 + case .notifications: + return 1005 + case .report: + return 1006 + case .leave: + return 1007 + } + } + + func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool { + guard let entry = entry as? ChannelInfoEntry else { + return false + } + return self.sortIndex < entry.sortIndex + } + + func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem { + switch self { + case let .info(peer, cachedData): + return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .plain) + case let .about(text): + return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue) + case let .userName(value): + return PeerInfoTextWithLabelItem(label: "share link", text: "https://telegram.me/\(value)", multiline: false, sectionId: self.section.rawValue) + return PeerInfoActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { + + }) + case .sharedMedia: + return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: self.section.rawValue, style: .plain, action: { + interaction.openSharedMedia() + }) + case let .notifications(settings): + 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: .plain, action: { + interaction.changeNotificationNoteSettings() + }) + case .report: + return PeerInfoActionItem(title: "Report", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { + + }) + case .leave: + return PeerInfoActionItem(title: "Leave Channel", kind: .destructive, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { + + }) + } + } +} + +func channelBroadcastInfoEntries(view: PeerView) -> [PeerInfoEntry] { + var entries: [PeerInfoEntry] = [] + entries.append(ChannelInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData)) + if let cachedChannelData = view.cachedData as? CachedChannelData { + if let about = cachedChannelData.about, !about.isEmpty { + entries.append(ChannelInfoEntry.about(text: about)) + } + } + if let channel = view.peers[view.peerId] as? TelegramChannel { + if let username = channel.username, !username.isEmpty { + entries.append(ChannelInfoEntry.userName(value: username)) + } + entries.append(ChannelInfoEntry.sharedMedia) + entries.append(ChannelInfoEntry.notifications(settings: view.notificationSettings)) + entries.append(ChannelInfoEntry.report) + if channel.participationStatus == .member { + entries.append(ChannelInfoEntry.leave) + } + } + return entries +} diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 69890e183b..f14b96163b 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -446,10 +446,9 @@ public class ChatController: ViewController { self.navigationActionDisposable.set((self.peerView.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self, let peer = peerView.peers[peerView.peerId] { - if let chatInfoController = chatInfoController(account: strongSelf.account, peer: peer) { - (strongSelf.navigationController as? NavigationController)?.pushViewController(chatInfoController) - } + if let strongSelf = self, let _ = peerView.peers[peerView.peerId] { + let chatInfoController = PeerInfoController(account: strongSelf.account, peerId: peerView.peerId) + (strongSelf.navigationController as? NavigationController)?.pushViewController(chatInfoController) } })) break diff --git a/TelegramUI/ChatInfo.swift b/TelegramUI/ChatInfo.swift index 67263c82fa..28efe73f69 100644 --- a/TelegramUI/ChatInfo.swift +++ b/TelegramUI/ChatInfo.swift @@ -3,22 +3,6 @@ import Postbox import TelegramCore import Display -func chatInfoController(account: Account, peer: Peer) -> ViewController? { - if let user = peer as? TelegramUser { - return UserInfoController(account: account, peerId: peer.id) - } else if let channel = peer as? TelegramChannel { - switch channel.info { - case .broadcast: - return ChannelBroadcastInfoController(account: account, peerId: peer.id) - case .group: - break - } - } else { - return PeerMediaCollectionController(account: account, peerId: peer.id) - } - return nil -} - func peerSharedMediaController(account: Account, peerId: PeerId) -> ViewController? { return PeerMediaCollectionController(account: account, peerId: peerId) } diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 364e57dd1d..d385d31a3f 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -30,12 +30,15 @@ func ==(lhs: ChatListMessageViewPosition, rhs: ChatListMessageViewPosition) -> B private enum ChatListControllerEntryId: Hashable, CustomStringConvertible { case Search + case Hole(Int64) case PeerId(Int64) var hashValue: Int { switch self { case .Search: return 0 + case let .Hole(peerId): + return peerId.hashValue case let .PeerId(peerId): return peerId.hashValue } @@ -45,6 +48,8 @@ private enum ChatListControllerEntryId: Hashable, CustomStringConvertible { switch self { case .Search: return "search" + case let .Hole(value): + return "hole(\(value))" case let .PeerId(value): return "peerId(\(value))" } @@ -64,6 +69,13 @@ private func ==(lhs: ChatListControllerEntryId, rhs: ChatListControllerEntryId) default: return false } + case let .Hole(lhsId): + switch rhs { + case .Hole(lhsId): + return true + default: + return false + } case let .PeerId(lhsId): switch rhs { case let .PeerId(rhsId): @@ -97,8 +109,12 @@ private enum ChatListControllerEntry: Comparable, Identifiable { switch self { case .SearchEntry: return .Search - default: - return .PeerId(self.index.id.peerId.toInt64()) + case let .MessageEntry(message, _, _): + return .PeerId(message.id.peerId.toInt64()) + case let .HoleEntry(hole): + return .Hole(Int64(hole.index.id.id)) + case let .Nothing(index): + return .PeerId(index.id.peerId.toInt64()) } } } @@ -302,7 +318,7 @@ public class ChatListController: ViewController { animated = false } - strongSelf.setPeerView(view, firstTime: strongSelf.chatListViewAndEntries == nil, scrollPosition: firstTime ?scrollPosition : nil, animated: animated) + strongSelf.setPeerView(view, firstTime: strongSelf.chatListViewAndEntries == nil, scrollPosition: firstTime ? scrollPosition : nil, animated: animated) firstTime = false } })) diff --git a/TelegramUI/ChatListEmptyItem.swift b/TelegramUI/ChatListEmptyItem.swift index 47c1afe52e..a8e006d700 100644 --- a/TelegramUI/ChatListEmptyItem.swift +++ b/TelegramUI/ChatListEmptyItem.swift @@ -3,6 +3,7 @@ import UIKit import AsyncDisplayKit import Postbox import Display +import SwiftSignalKit class ChatListEmptyItem: ListViewItem { let selectable: Bool = false @@ -18,6 +19,18 @@ class ChatListEmptyItem: ListViewItem { completion(node, {}) } } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + assert(node is ChatListEmptyItemNode) + if let node = node as? ChatListEmptyItemNode { + Queue.mainQueue().async { + node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) + node.updateItemPosition(first: previousItem == nil, last: nextItem == nil) + + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), {}) + } + } + } } private let separatorHeight = 1.0 / UIScreen.main.scale diff --git a/TelegramUI/ChatListHoleItem.swift b/TelegramUI/ChatListHoleItem.swift index 5ad19e50fa..981d805031 100644 --- a/TelegramUI/ChatListHoleItem.swift +++ b/TelegramUI/ChatListHoleItem.swift @@ -24,6 +24,7 @@ class ChatListHoleItem: ListViewItem { } func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + assert(node is ChatListHoleItemNode) if let node = node as? ChatListHoleItemNode { Queue.mainQueue().async { let layout = node.asyncLayout() diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 3f5021c131..c056b157f1 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -35,6 +35,7 @@ class ChatListItem: ListViewItem { } func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + assert(node is ChatListItemNode) if let node = node as? ChatListItemNode { Queue.mainQueue().async { node.setupItem(account: self.account, message: self.message, combinedReadState: self.combinedReadState, notificationSettings: self.notificationSettings) diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index 699c44c43c..fda8677c16 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -5,6 +5,7 @@ import TelegramCore private let messageFont: UIFont = UIFont.systemFont(ofSize: 17.0) private let messageBoldFont: UIFont = UIFont.boldSystemFont(ofSize: 17.0) +private let messageFixedFont: UIFont = UIFont(name: "Menlo-Regular", size: 16.0) ?? UIFont.systemFont(ofSize: 17.0) class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode @@ -87,8 +88,16 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { switch entity.type { case .Url: string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + case .Email: + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + case .TextUrl: + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) case .Bold: string.addAttribute(NSFontAttributeName, value: messageBoldFont, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + case .Mention: + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + case .Code, .Pre: + string.addAttribute(NSFontAttributeName, value: messageFixedFont, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) default: break } diff --git a/TelegramUI/GroupInfoEntries.swift b/TelegramUI/GroupInfoEntries.swift new file mode 100644 index 0000000000..b3414dfcef --- /dev/null +++ b/TelegramUI/GroupInfoEntries.swift @@ -0,0 +1,293 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed() + +private enum GroupInfoSection: UInt32, PeerInfoSection { + case info + case about + case sharedMediaAndNotifications + case members + case leave + + func isEqual(to: PeerInfoSection) -> Bool { + guard let section = to as? GroupInfoSection else { + return false + } + return section == self + } + + func isOrderedBefore(_ section: PeerInfoSection) -> Bool { + guard let section = section as? GroupInfoSection else { + return false + } + return self.rawValue < section.rawValue + } +} + +enum GroupInfoEntry: PeerInfoEntry { + case info(peer: Peer?, cachedData: CachedPeerData?) + case setGroupPhoto + case aboutHeader + case about(text: String) + case sharedMedia + case notifications(settings: PeerNotificationSettings?) + case usersHeader + case addMember + case member(index: Int, peer: Peer?) + case leave + + var section: PeerInfoSection { + switch self { + case .info, .setGroupPhoto: + return GroupInfoSection.info + case .aboutHeader, .about: + return GroupInfoSection.about + case .sharedMedia, .notifications: + return GroupInfoSection.sharedMediaAndNotifications + case .usersHeader, .addMember, .member: + return GroupInfoSection.members + case .leave: + return GroupInfoSection.leave + } + } + + func isEqual(to: PeerInfoEntry) -> Bool { + guard let entry = to as? GroupInfoEntry else { + return false + } + + switch self { + case let .info(lhsPeer, lhsCachedData): + switch entry { + case let .info(rhsPeer, rhsCachedData): + if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhsPeer == nil) != (rhsPeer != nil) { + return false + } + if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { + if !lhsCachedData.isEqual(to: rhsCachedData) { + return false + } + } else if (rhsCachedData == nil) != (rhsCachedData != nil) { + return false + } + return true + default: + return false + } + case .setGroupPhoto: + if case .setGroupPhoto = entry { + return true + } else { + return false + } + case .aboutHeader: + if case .aboutHeader = entry { + return true + } else { + return false + } + case let .about(lhsText): + switch entry { + case let .about(lhsText): + return true + default: + return false + } + case .sharedMedia: + switch entry { + case .sharedMedia: + return true + default: + return false + } + case let .notifications(lhsSettings): + switch entry { + case let .notifications(rhsSettings): + if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { + return lhsSettings.isEqual(to: rhsSettings) + } else if (lhsSettings != nil) != (rhsSettings != nil) { + return false + } + return true + default: + return false + } + case .usersHeader: + if case .usersHeader = entry { + return true + } else { + return false + } + case .addMember: + if case .addMember = entry { + return true + } else { + return false + } + case let .member(lhsIndex, lhsPeer): + if case let .member(rhsIndex, rhsPeer) = entry { + if lhsIndex != rhsIndex { + return false + } + if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhsPeer != nil) != (rhsPeer != nil) { + return false + } + return true + } else { + return false + } + case .leave: + if case .leave = entry { + return true + } else { + return false + } + } + } + + var stableId: Int { + return self.sortIndex + } + + private var sortIndex: Int { + switch self { + case .info: + return 0 + case .setGroupPhoto: + return 1 + case .aboutHeader: + return 2 + case .about: + return 3 + case .notifications: + return 4 + case .sharedMedia: + return 5 + case .usersHeader: + return 6 + case .addMember: + return 7 + case let .member(index, _): + return 10 + index + case .leave: + return 1000000 + } + } + + func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool { + guard let other = entry as? GroupInfoEntry else { + return false + } + + return self.sortIndex < other.sortIndex + } + + func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem { + switch self { + case let .info(peer, cachedData): + return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .blocks) + case .setGroupPhoto: + 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: { + }) + case .sharedMedia: + return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: self.section.rawValue, style: .blocks, action: { + interaction.openSharedMedia() + }) + case .addMember: + 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: { + if let peer = peer { + interaction.openPeerInfo(peer.id) + } + }) + case .leave: + return PeerInfoActionItem(title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section.rawValue, style: .blocks, action: { + }) + default: + preconditionFailure() + } + } +} + +func groupInfoEntries(view: PeerView) -> [PeerInfoEntry] { + var entries: [PeerInfoEntry] = [] + entries.append(GroupInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData)) + entries.append(GroupInfoEntry.setGroupPhoto) + + entries.append(GroupInfoEntry.notifications(settings: view.notificationSettings)) + entries.append(GroupInfoEntry.sharedMedia) + + entries.append(GroupInfoEntry.addMember) + + if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { + let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in + switch lhs { + case .creator: + return false + case let .admin(lhsId, _, lhsInvitedAt): + switch rhs { + case .creator: + return true + case let .admin(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .member(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + } + case let .member(lhsId, _, lhsInvitedAt): + switch rhs { + case .creator: + return true + case let .admin(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + case let .member(rhsId, _, rhsInvitedAt): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt + } + } + return false + }) + + for i in 0 ..< sortedParticipants.count { + if let peer = view.peers[sortedParticipants[i].peerId] { + entries.append(GroupInfoEntry.member(index: i, peer: peer)) + } + } + } + + if let group = view.peers[view.peerId] as? TelegramGroup { + if case .Member = group.membership { + entries.append(GroupInfoEntry.leave) + } + } + + return entries +} diff --git a/TelegramUI/PeerInfoActionItem.swift b/TelegramUI/PeerInfoActionItem.swift index eeed1e2966..e74426beb9 100644 --- a/TelegramUI/PeerInfoActionItem.swift +++ b/TelegramUI/PeerInfoActionItem.swift @@ -8,23 +8,32 @@ enum PeerInfoActionKind { case destructive } +enum PeerInfoActionAlignment { + case natural + case center +} + class PeerInfoActionItem: ListViewItem, PeerInfoItem { let title: String let kind: PeerInfoActionKind + let alignment: PeerInfoActionAlignment let sectionId: PeerInfoItemSectionId + let style: PeerInfoListStyle let action: () -> Void - init(title: String, kind: PeerInfoActionKind, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) { + init(title: String, kind: PeerInfoActionKind, alignment: PeerInfoActionAlignment, sectionId: PeerInfoItemSectionId, style: PeerInfoListStyle, action: @escaping () -> Void) { self.title = title self.kind = kind + self.alignment = alignment self.sectionId = sectionId + self.style = style self.action = action } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { async { let node = PeerInfoActionItemNode() - let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -41,7 +50,7 @@ class PeerInfoActionItem: ListViewItem, PeerInfoItem { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -63,11 +72,26 @@ class PeerInfoActionItem: ListViewItem, PeerInfoItem { private let titleFont = Font.regular(17.0) class PeerInfoActionItemNode: ListViewItemNode { - let titleNode: TextNode - let separatorNode: ASDisplayNode + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode + let titleNode: TextNode + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.bottomStripeNode.isLayerBacked = true + self.titleNode = TextNode() self.titleNode.isLayerBacked = true self.titleNode.contentMode = .left @@ -77,35 +101,106 @@ class PeerInfoActionItemNode: ListViewItemNode { self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - self.separatorNode.displaysAsynchronously = false - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) - super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.separatorNode) self.addSubnode(self.titleNode) } - func asyncLayout() -> (_ item: PeerInfoActionItem, _ width: CGFloat, _ insets: UIEdgeInsets) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: PeerInfoActionItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) - return { item, width, insets in + 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 contentSize = CGSize(width: width, height: 44.0) - return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + switch item.style { + case .plain: + contentSize = CGSize(width: width, height: 44.0) + insets = peerInfoItemNeighborsPlainInsets(neighbors) + case .blocks: + contentSize = CGSize(width: width, height: 44.0) + let topInset: CGFloat + switch neighbors.top { + case .sameSection, .none: + topInset = 0.0 + case .otherSection: + topInset = separatorHeight + 35.0 + } + let bottomInset: CGFloat + switch neighbors.bottom { + case .sameSection, .otherSection: + bottomInset = 0.0 + case .none: + bottomInset = separatorHeight + 35.0 + } + insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0) + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in if let strongSelf = self { let _ = titleApply() - let leftInset: CGFloat = 35.0 + let leftInset: CGFloat - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) + switch item.style { + case .plain: + leftInset = 35.0 + + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight)) + case .blocks: + leftInset = 16.0 + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection: + strongSelf.topStripeNode.isHidden = true + case .none, .otherSection: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection: + bottomStripeInset = 16.0 + case .none, .otherSection: + bottomStripeInset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) + switch item.alignment { + case .natural: + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) + case .center: + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleLayout.size.width) / 2.0), y: 12.0), size: titleLayout.size) + } strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } @@ -119,7 +214,19 @@ class PeerInfoActionItemNode: ListViewItemNode { if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } } } else { if self.highlightedBackgroundNode.supernode != nil { diff --git a/TelegramUI/PeerInfoAvatarAndNameItem.swift b/TelegramUI/PeerInfoAvatarAndNameItem.swift index d0920dd47a..f6303eee43 100644 --- a/TelegramUI/PeerInfoAvatarAndNameItem.swift +++ b/TelegramUI/PeerInfoAvatarAndNameItem.swift @@ -10,18 +10,20 @@ class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem { let peer: Peer? let cachedData: CachedPeerData? let sectionId: PeerInfoItemSectionId + let style: PeerInfoListStyle - init(account: Account, peer: Peer?, cachedData: CachedPeerData?, sectionId: PeerInfoItemSectionId) { + init(account: Account, peer: Peer?, cachedData: CachedPeerData?, sectionId: PeerInfoItemSectionId, style: PeerInfoListStyle) { self.account = account self.peer = peer self.cachedData = cachedData self.sectionId = sectionId + self.style = style } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { async { let node = PeerInfoAvatarAndNameItemNode() - let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -38,7 +40,7 @@ class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -60,12 +62,27 @@ private let nameFont = Font.medium(19.0) private let statusFont = Font.regular(15.0) class PeerInfoAvatarAndNameItemNode: ListViewItemNode { - let avatarNode: AvatarNode + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode - let nameNode: TextNode - let statusNode: TextNode + private let avatarNode: AvatarNode + private let nameNode: TextNode + private let statusNode: TextNode init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.bottomStripeNode.isLayerBacked = true + self.avatarNode = AvatarNode(font: Font.regular(20.0)) self.nameNode = TextNode() @@ -85,11 +102,11 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { self.addSubnode(self.statusNode) } - func asyncLayout() -> (_ item: PeerInfoAvatarAndNameItem, _ width: CGFloat, _ insets: UIEdgeInsets) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: PeerInfoAvatarAndNameItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let layoutNameNode = TextNode.asyncLayout(self.nameNode) let layoutStatusNode = TextNode.asyncLayout(self.statusNode) - return { item, width, insets in + return { item, width, neighbors in let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: item.peer?.displayTitle ?? "", font: nameFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) let statusText: String @@ -121,8 +138,72 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 96.0), insets: insets), { [weak self] in + let separatorHeight = UIScreenPixel + + let contentSize: CGSize + let insets: UIEdgeInsets + switch item.style { + case .plain: + contentSize = CGSize(width: width, height: 96.0) + insets = peerInfoItemNeighborsPlainInsets(neighbors) + case .blocks: + contentSize = CGSize(width: width, height: 92.0) + let topInset: CGFloat + switch neighbors.top { + case .sameSection, .none: + topInset = 0.0 + case .otherSection: + topInset = separatorHeight + 35.0 + } + insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0) + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in if let strongSelf = self { + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode != nil { + strongSelf.bottomStripeNode.removeFromSupernode() + } + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection: + strongSelf.topStripeNode.isHidden = true + case .none, .otherSection: + strongSelf.topStripeNode.isHidden = false + } + + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection: + bottomStripeInset = 16.0 + case .none, .otherSection: + bottomStripeInset = 0.0 + } + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: CGSize(width: width, height: layoutSize.height)) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: layoutSize.height - insets.top - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } + let _ = nameNodeApply() let _ = statusNodeApply() @@ -133,7 +214,6 @@ class PeerInfoAvatarAndNameItemNode: ListViewItemNode { strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 15.0, y: 15.0), 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 new file mode 100644 index 0000000000..e9d6102abf --- /dev/null +++ b/TelegramUI/PeerInfoController.swift @@ -0,0 +1,197 @@ +import Foundation +import Display +import Postbox +import SwiftSignalKit +import TelegramCore + +final class PeerInfoControllerInteraction { + let openSharedMedia: () -> Void + let changeNotificationNoteSettings: () -> Void + let openPeerInfo: (PeerId) -> Void + + init(openSharedMedia: @escaping () -> Void, changeNotificationNoteSettings: @escaping () -> Void, openPeerInfo: @escaping (PeerId) -> Void) { + self.openSharedMedia = openSharedMedia + self.changeNotificationNoteSettings = changeNotificationNoteSettings + self.openPeerInfo = openPeerInfo + } +} + +private struct PeerInfoSortableEntry: Identifiable, Comparable { + let entry: PeerInfoEntry + + var stableId: Int { + return self.entry.stableId + } + + static func ==(lhs: PeerInfoSortableEntry, rhs: PeerInfoSortableEntry) -> Bool { + return lhs.entry.isEqual(to: rhs.entry) + } + + static func <(lhs: PeerInfoSortableEntry, rhs: PeerInfoSortableEntry) -> Bool { + return lhs.entry.isOrderedBefore(rhs.entry) + } +} + +private struct PeerInfoEntryTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] +} + +private func preparedPeerInfoEntryTransition(account: Account, from fromEntries: [PeerInfoSortableEntry], to toEntries: [PeerInfoSortableEntry], interaction: PeerInfoControllerInteraction) -> PeerInfoEntryTransition { + 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.entry.item(account: account, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.entry.item(account: account, interaction: interaction), directionHint: nil) } + + return PeerInfoEntryTransition(deletions: deletions, insertions: insertions, updates: updates) +} + +public final class PeerInfoController: ListController { + private let account: Account + private let peerId: PeerId + + private var _ready = Promise() + override public var ready: Promise { + return self._ready + } + private var didSetReady = false + + private let transitionDisposable = MetaDisposable() + private let changeSettingsDisposable = MetaDisposable() + + private var currentListStyle: PeerInfoListStyle = .plain + + public init(account: Account, peerId: PeerId) { + self.account = account + self.peerId = peerId + + super.init() + + self.title = "Info" + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.transitionDisposable.dispose() + self.changeSettingsDisposable.dispose() + } + + override public func displayNodeDidLoad() { + super.displayNodeDidLoad() + + let interaction = PeerInfoControllerInteraction(openSharedMedia: { [weak self] in + if let strongSelf = self { + if let controller = peerSharedMediaController(account: strongSelf.account, peerId: strongSelf.peerId) { + (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) + } + } + }, changeNotificationNoteSettings: { [weak self] in + if let strongSelf = self { + let controller = ActionSheetController() + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + let notificationAction: (Int32) -> Void = { [weak strongSelf] muteUntil in + if let strongSelf = strongSelf { + let muteState: PeerMuteState + if muteUntil <= 0 { + muteState = .unmuted + } else if muteUntil == Int32.max { + muteState = .muted(until: Int32.max) + } else { + muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) + } + strongSelf.changeSettingsDisposable.set(changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.appDefault)).start()) + } + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Enable", action: { + dismissAction() + notificationAction(0) + }), + ActionSheetButtonItem(title: "Mute for 1 hour", action: { + dismissAction() + notificationAction(1 * 60 * 60) + }), + ActionSheetButtonItem(title: "Mute for 8 hours", action: { + dismissAction() + notificationAction(8 * 60 * 60) + }), + ActionSheetButtonItem(title: "Mute for 2 days", action: { + dismissAction() + notificationAction(2 * 24 * 60 * 60) + }), + ActionSheetButtonItem(title: "Disable", action: { + dismissAction() + notificationAction(Int32.max) + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) + ]) + strongSelf.present(controller, in: .window) + } + }, openPeerInfo: { [weak self] peerId in + if let strongSelf = self { + let controller = PeerInfoController(account: strongSelf.account, peerId: peerId) + (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) + } + }) + + self.listDisplayNode.backgroundColor = UIColor.white + + let previousEntries = Atomic<[PeerInfoSortableEntry]?>(value: nil) + + let account = self.account + let transition = account.viewTracker.peerView(self.peerId) + |> map { view -> (PeerInfoEntryTransition, PeerInfoListStyle, Bool, Bool) in + let entries = peerInfoEntries(view: view).map { PeerInfoSortableEntry(entry: $0) } + assert(entries == entries.sorted()) + let previous = previousEntries.swap(entries) + let style: PeerInfoListStyle + if let group = view.peers[view.peerId] as? TelegramGroup { + style = .blocks + } else { + style = .plain + } + return (preparedPeerInfoEntryTransition(account: account, from: previous ?? [], to: entries, interaction: interaction), style, previous == nil, previous != nil) + } + |> deliverOnMainQueue + + self.transitionDisposable.set(transition.start(next: { [weak self] (transition, style, firstTime, animated) in + self?.enqueueTransition(transition, style: style, firstTime: firstTime, animated: animated) + })) + } + + private func enqueueTransition(_ transition: PeerInfoEntryTransition, style: PeerInfoListStyle, firstTime: Bool, animated: Bool) { + if self.currentListStyle != style { + self.currentListStyle = style + switch style { + case .plain: + self.listDisplayNode.backgroundColor = .white + case .blocks: + self.listDisplayNode.backgroundColor = UIColor(0xefeff4) + } + } + var options = ListViewDeleteAndInsertOptions() + if firstTime { + options.insert(.Synchronous) + options.insert(.LowLatency) + } else if animated { + options.insert(.AnimateInsertion) + } + self.listDisplayNode.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)) + } + } + }) + } +} diff --git a/TelegramUI/PeerInfoDisclosureItem.swift b/TelegramUI/PeerInfoDisclosureItem.swift index 3942a6d46f..8ce6b242cb 100644 --- a/TelegramUI/PeerInfoDisclosureItem.swift +++ b/TelegramUI/PeerInfoDisclosureItem.swift @@ -7,19 +7,21 @@ class PeerInfoDisclosureItem: ListViewItem, PeerInfoItem { let title: String let label: String let sectionId: PeerInfoItemSectionId + let style: PeerInfoListStyle let action: () -> Void - init(title: String, label: String, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) { + init(title: String, label: String, sectionId: PeerInfoItemSectionId, style: PeerInfoListStyle, action: @escaping () -> Void) { self.title = title self.label = label self.sectionId = sectionId + self.style = style self.action = action } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { async { let node = PeerInfoDisclosureItemNode() - let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -36,7 +38,7 @@ class PeerInfoDisclosureItem: ListViewItem, PeerInfoItem { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -59,13 +61,28 @@ private let titleFont = Font.regular(17.0) private let arrowImage = UIImage(bundleImageName: "Peer Info/DisclosureArrow")?.precomposed() class PeerInfoDisclosureItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + let titleNode: TextNode let labelNode: TextNode let arrowNode: ASImageNode - let separatorNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.bottomStripeNode.isLayerBacked = true + self.titleNode = TextNode() self.titleNode.isLayerBacked = true @@ -82,37 +99,98 @@ class PeerInfoDisclosureItemNode: ListViewItemNode { self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - self.separatorNode.displaysAsynchronously = false - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) - super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.separatorNode) self.addSubnode(self.titleNode) self.addSubnode(self.labelNode) self.addSubnode(self.arrowNode) } - func asyncLayout() -> (_ item: PeerInfoDisclosureItem, _ width: CGFloat, _ insets: UIEdgeInsets) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: PeerInfoDisclosureItem, _ width: CGFloat, _ insets: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) - return { item, width, insets in + return { item, width, neighbors in let sectionInset: CGFloat = 22.0 let rightInset: CGFloat = 34.0 + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + switch item.style { + case .plain: + contentSize = CGSize(width: width, height: 44.0) + insets = peerInfoItemNeighborsPlainInsets(neighbors) + case .blocks: + contentSize = CGSize(width: width, height: 44.0) + let topInset: CGFloat + switch neighbors.top { + case .sameSection, .none: + topInset = 0.0 + case .otherSection: + topInset = separatorHeight + 35.0 + } + insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0) + } + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: titleFont, textColor: UIColor(0x8e8e93)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), nil) - let contentSize = CGSize(width: width, height: 44.0) + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in if let strongSelf = self { let _ = titleApply() let _ = labelApply() - let leftInset: CGFloat = 35.0 + let leftInset: CGFloat + + switch item.style { + case .plain: + leftInset = 35.0 + + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight)) + case .blocks: + leftInset = 16.0 + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection: + strongSelf.topStripeNode.isHidden = true + case .none, .otherSection: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection: + bottomStripeInset = 16.0 + case .none, .otherSection: + bottomStripeInset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: layoutSize.height - insets.top - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: width - rightInset - labelLayout.size.width, y: 12.0), size: labelLayout.size) @@ -121,8 +199,6 @@ class PeerInfoDisclosureItemNode: ListViewItemNode { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: width - 15.0 - arrowImage.size.width, y: 18.0), size: arrowImage.size) } - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) } }) @@ -135,7 +211,19 @@ class PeerInfoDisclosureItemNode: ListViewItemNode { if highlighted { self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } } } else { if self.highlightedBackgroundNode.supernode != nil { diff --git a/TelegramUI/PeerInfoEntries.swift b/TelegramUI/PeerInfoEntries.swift new file mode 100644 index 0000000000..b3ab33a421 --- /dev/null +++ b/TelegramUI/PeerInfoEntries.swift @@ -0,0 +1,38 @@ +import Foundation +import Postbox +import TelegramCore +import Display + +protocol PeerInfoSection { + var rawValue: UInt32 { get } + func isEqual(to: PeerInfoSection) -> Bool + func isOrderedBefore(_ section: PeerInfoSection) -> Bool +} + +protocol PeerInfoEntryStableId { + func isEqual(to: PeerInfoEntryStableId) -> Bool +} + +protocol PeerInfoEntry { + var section: PeerInfoSection { get } + var stableId: Int { get } + func isEqual(to: PeerInfoEntry) -> Bool + func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool + func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem +} + +func peerInfoEntries(view: PeerView) -> [PeerInfoEntry] { + if let user = view.peers[view.peerId] as? TelegramUser { + return userInfoEntries(view: view) + } else if let channel = view.peers[view.peerId] as? TelegramChannel { + switch channel.info { + case .broadcast: + return channelBroadcastInfoEntries(view: view) + case .group: + return [] + } + } else if let group = view.peers[view.peerId] as? TelegramGroup { + return groupInfoEntries(view: view) + } + return [] +} diff --git a/TelegramUI/PeerInfoItem.swift b/TelegramUI/PeerInfoItem.swift index ac2b083ab0..83e0afbe90 100644 --- a/TelegramUI/PeerInfoItem.swift +++ b/TelegramUI/PeerInfoItem.swift @@ -5,14 +5,61 @@ protocol PeerInfoItem { var sectionId: PeerInfoItemSectionId { get } } -func peerInfoItemInsets(item: PeerInfoItem, topItem: PeerInfoItem?, bottomItem: PeerInfoItem?) -> UIEdgeInsets { - var insets = UIEdgeInsets() - if let topItem = topItem, topItem.sectionId != item.sectionId { - insets.top += 22.0 - } - if bottomItem == nil { - insets.bottom += 22.0 +enum PeerInfoItemNeighbor { + case none + case otherSection + case sameSection +} + +struct PeerInfoItemNeighbors { + let top: PeerInfoItemNeighbor + let bottom: PeerInfoItemNeighbor +} + +func peerInfoItemNeighbors(item: PeerInfoItem, topItem: PeerInfoItem?, bottomItem: PeerInfoItem?) -> PeerInfoItemNeighbors { + let topNeighbor: PeerInfoItemNeighbor + if let topItem = topItem { + if topItem.sectionId != item.sectionId { + topNeighbor = .otherSection + } else { + topNeighbor = .sameSection + } + } else { + topNeighbor = .none } + let bottomNeighbor: PeerInfoItemNeighbor + if let bottomItem = bottomItem { + if bottomItem.sectionId != item.sectionId { + bottomNeighbor = .otherSection + } else { + bottomNeighbor = .sameSection + } + } else { + bottomNeighbor = .none + } + + return PeerInfoItemNeighbors(top: topNeighbor, bottom: bottomNeighbor) +} + +enum PeerInfoListStyle { + case plain + case blocks +} + +func peerInfoItemNeighborsPlainInsets(_ neighbors: PeerInfoItemNeighbors) -> UIEdgeInsets { + var insets = UIEdgeInsets() + switch neighbors.top { + case .otherSection: + insets.top += 22.0 + case .none, .sameSection: + break + } + switch neighbors.bottom { + case .none: + insets.bottom += 22.0 + case .otherSection, .sameSection: + break + } return insets } diff --git a/TelegramUI/PeerInfoPeerActionItem.swift b/TelegramUI/PeerInfoPeerActionItem.swift new file mode 100644 index 0000000000..e71ddfab2e --- /dev/null +++ b/TelegramUI/PeerInfoPeerActionItem.swift @@ -0,0 +1,207 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +class PeerInfoPeerActionItem: ListViewItem, PeerInfoItem { + let icon: UIImage? + let title: String + let sectionId: PeerInfoItemSectionId + let action: () -> Void + + init(icon: UIImage?, title: String, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) { + self.icon = icon + self.title = title + self.sectionId = sectionId + self.action = action + } + + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + async { + let node = PeerInfoPeerActionItemNode() + let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + apply() + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? PeerInfoPeerActionItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } + + var selectable: Bool = true + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action() + } +} + +private let titleFont = Font.regular(17.0) + +class PeerInfoPeerActionItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + private let iconNode: ASImageNode + private let titleNode: TextNode + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.bottomStripeNode.isLayerBacked = true + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displayWithoutProcessing = true + self.iconNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.iconNode) + self.addSubnode(self.titleNode) + } + + func asyncLayout() -> (_ item: PeerInfoPeerActionItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + + 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 contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + contentSize = CGSize(width: width, height: 44.0) + let topInset: CGFloat + switch neighbors.top { + case .sameSection, .none: + topInset = 0.0 + case .otherSection: + topInset = separatorHeight + 35.0 + } + insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + let _ = titleApply() + + strongSelf.iconNode.image = item.icon + if let image = item.icon { + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) + } + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection: + strongSelf.topStripeNode.isHidden = true + case .none, .otherSection: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection: + bottomStripeInset = leftInset + case .none, .otherSection: + bottomStripeInset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } +} diff --git a/TelegramUI/PeerInfoPeerItem.swift b/TelegramUI/PeerInfoPeerItem.swift new file mode 100644 index 0000000000..3daaa2fbe5 --- /dev/null +++ b/TelegramUI/PeerInfoPeerItem.swift @@ -0,0 +1,255 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +class PeerInfoPeerItem: ListViewItem, PeerInfoItem { + let account: Account + let peer: Peer? + let sectionId: PeerInfoItemSectionId + let action: () -> Void + + init(account: Account, peer: Peer?, sectionId: PeerInfoItemSectionId, action: @escaping () -> Void) { + self.account = account + self.peer = peer + self.sectionId = sectionId + self.action = action + } + + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + async { + let node = PeerInfoPeerItemNode() + let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + apply() + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? PeerInfoPeerItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } + + var selectable: Bool = true + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action() + } +} + +private let titleFont = Font.regular(17.0) +private let titleBoldFont = Font.medium(17.0) +private let statusFont = Font.regular(14.0) +private let avatarFont = Font.regular(17.0) + +class PeerInfoPeerItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + private let avatarNode: AvatarNode + private let titleNode: TextNode + private let statusNode: TextNode + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.bottomStripeNode.isLayerBacked = true + + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.isLayerBacked = true + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.statusNode = TextNode() + self.statusNode.isLayerBacked = true + self.statusNode.contentMode = .left + self.statusNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.avatarNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.statusNode) + } + + func asyncLayout() -> (_ item: PeerInfoPeerItem, _ width: CGFloat, _ neighbors: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + + return { item, width, neighbors in + var titleAttributedString: NSAttributedString? + var statusAttributedString: NSAttributedString? + + if 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)) + } + + 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 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 contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + contentSize = CGSize(width: width, height: 48.0) + let topInset: CGFloat + let bottomInset: CGFloat + switch neighbors.top { + case .sameSection, .none: + topInset = 0.0 + case .otherSection: + topInset = separatorHeight + 35.0 + } + switch neighbors.bottom { + case .none: + bottomInset = separatorHeight + 35.0 + case .otherSection, .sameSection: + bottomInset = separatorHeight + } + insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomInset, right: 0.0) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + let _ = titleApply() + let _ = statusApply() + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection: + strongSelf.topStripeNode.isHidden = true + case .none, .otherSection: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection: + bottomStripeInset = leftInset + case .none, .otherSection: + bottomStripeInset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + 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.avatarNode.frame = CGRect(origin: CGPoint(x: 14.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)) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } +} diff --git a/TelegramUI/PeerInfoTextWithLabelItem.swift b/TelegramUI/PeerInfoTextWithLabelItem.swift index 7c1984bb47..834bab29cc 100644 --- a/TelegramUI/PeerInfoTextWithLabelItem.swift +++ b/TelegramUI/PeerInfoTextWithLabelItem.swift @@ -19,7 +19,7 @@ final class PeerInfoTextWithLabelItem: ListViewItem, PeerInfoItem { func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { async { let node = PeerInfoTextWithLabelItemNode() - let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) node.contentSize = layout.contentSize node.insets = layout.insets @@ -36,7 +36,7 @@ final class PeerInfoTextWithLabelItem: ListViewItem, PeerInfoItem { let makeLayout = node.asyncLayout() async { - let (layout, apply) = makeLayout(self, width, peerInfoItemInsets(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) + let (layout, apply) = makeLayout(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) Queue.mainQueue().async { completion(layout, { apply() @@ -85,11 +85,12 @@ class PeerInfoTextWithLabelItemNode: ListViewItemNode { self.addSubnode(self.textNode) } - func asyncLayout() -> (_ item: PeerInfoTextWithLabelItem, _ width: CGFloat, _ insets: UIEdgeInsets) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: PeerInfoTextWithLabelItem, _ width: CGFloat, _ insets: PeerInfoItemNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) - return { item, width, insets in + return { item, width, neighbors in + 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) diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift deleted file mode 100644 index b5e975dd7b..0000000000 --- a/TelegramUI/UserInfoController.swift +++ /dev/null @@ -1,386 +0,0 @@ -import Foundation -import Display -import Postbox -import SwiftSignalKit -import TelegramCore - -private enum UserInfoSection: UInt32 { - case info - case actions - case sharedMediaAndNotifications - case block -} - -private enum UserInfoEntry: Comparable, Identifiable { - case info(peer: Peer?, cachedData: CachedPeerData?) - case about(text: String) - case phoneNumber(index: Int, value: PhoneNumberWithLabel) - case userName(value: String) - case sendMessage - case shareContact - case startSecretChat - case sharedMedia - case notifications(settings: PeerNotificationSettings?) - case block - - fileprivate var section: UserInfoSection { - switch self { - case .info, .about, .phoneNumber, .userName: - return .info - case .sendMessage, .shareContact, .startSecretChat: - return .actions - case .sharedMedia, .notifications: - return .sharedMediaAndNotifications - case .block: - return .block - } - } - - fileprivate var stableId: Int { - return self.sortIndex - } - - fileprivate static func ==(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool { - switch lhs { - case let .info(lhsPeer, lhsCachedData): - switch rhs { - case let .info(rhsPeer, rhsCachedData): - if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { - if !lhsPeer.isEqual(rhsPeer) { - return false - } - } else if (lhsPeer == nil) != (rhsPeer != nil) { - return false - } - if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { - if !lhsCachedData.isEqual(to: rhsCachedData) { - return false - } - } else if (rhsCachedData == nil) != (rhsCachedData != nil) { - return false - } - return true - default: - return false - } - case let .about(lhsText): - switch rhs { - case let .about(lhsText): - return true - default: - return false - } - case let .phoneNumber(lhsIndex, lhsValue): - switch rhs { - case let .phoneNumber(rhsIndex, rhsValue) where lhsIndex == rhsIndex && lhsValue == rhsValue: - return true - default: - return false - } - case let .userName(value): - switch rhs { - case .userName(value): - return true - default: - return false - } - case .sendMessage: - switch rhs { - case .sendMessage: - return true - default: - return false - } - case .shareContact: - switch rhs { - case .shareContact: - return true - default: - return false - } - case .startSecretChat: - switch rhs { - case .startSecretChat: - return true - default: - return false - } - case .sharedMedia: - switch rhs { - case .sharedMedia: - return true - default: - return false - } - case let .notifications(lhsSettings): - switch rhs { - case let .notifications(rhsSettings): - if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { - return lhsSettings.isEqual(to: rhsSettings) - } else if (lhsSettings != nil) != (rhsSettings != nil) { - return false - } - return true - default: - return false - } - case .block: - switch rhs { - case .block: - return true - default: - return false - } - } - } - - private var sortIndex: Int { - switch self { - case .info: - return 0 - case .about: - return 1 - case let .phoneNumber(index, _): - return 2 + index - case .userName: - return 1000 - case .sendMessage: - return 1001 - case .shareContact: - return 1002 - case .startSecretChat: - return 1003 - case .sharedMedia: - return 1004 - case .notifications: - return 1005 - case .block: - return 1006 - } - } - - fileprivate static func <(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool { - return lhs.sortIndex < rhs.sortIndex - } -} - -private func userInfoEntries(account: Account, peerId: PeerId) -> Signal<[UserInfoEntry], NoError> { - return account.viewTracker.peerView(peerId) - |> map { view -> [UserInfoEntry] in - var entries: [UserInfoEntry] = [] - entries.append(.info(peer: view.peers[peerId], cachedData: view.cachedData)) - if let cachedUserData = view.cachedData as? CachedUserData { - if let about = cachedUserData.about, !about.isEmpty { - entries.append(.about(text: about)) - } - } - if let user = view.peers[peerId] as? TelegramUser { - if let phoneNumber = user.phone, !phoneNumber.isEmpty { - entries.append(.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber))) - } - if let username = user.username, !username.isEmpty { - entries.append(.userName(value: username)) - } - entries.append(.sendMessage) - entries.append(.shareContact) - entries.append(.startSecretChat) - entries.append(.sharedMedia) - entries.append(.notifications(settings: view.notificationSettings)) - entries.append(.block) - } - return entries - } -} - -private struct UserInfoEntryTransition { - let deletions: [ListViewDeleteItem] - let insertions: [ListViewInsertItem] - let updates: [ListViewUpdateItem] -} - -private func infoItemForEntry(account: Account, entry: UserInfoEntry, interaction: PeerInfoControllerInteraction) -> ListViewItem { - switch entry { - case let .info(peer, cachedData): - return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: entry.section.rawValue) - case let .about(text): - return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: entry.section.rawValue) - case let .phoneNumber(_, value): - return PeerInfoTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: entry.section.rawValue) - case let .userName(value): - return PeerInfoTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: entry.section.rawValue) - case .sendMessage: - return PeerInfoActionItem(title: "Send Message", kind: .generic, sectionId: entry.section.rawValue, action: { - - }) - case .shareContact: - return PeerInfoActionItem(title: "Share Contact", kind: .generic, sectionId: entry.section.rawValue, action: { - - }) - case .startSecretChat: - return PeerInfoActionItem(title: "Start Secret Chat", kind: .generic, sectionId: entry.section.rawValue, action: { - - }) - case .sharedMedia: - return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: entry.section.rawValue, action: { - interaction.openSharedMedia() - }) - case let .notifications(settings): - 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: entry.section.rawValue, action: { - interaction.changeNotificationNoteSettings() - }) - case .block: - return PeerInfoActionItem(title: "Block User", kind: .destructive, sectionId: entry.section.rawValue, action: { - - }) - } -} - -private func preparedUserInfoEntryTransition(account: Account, from fromEntries: [UserInfoEntry], to toEntries: [UserInfoEntry], interaction: PeerInfoControllerInteraction) -> UserInfoEntryTransition { - 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: infoItemForEntry(account: account, entry: $0.1, interaction: interaction), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: infoItemForEntry(account: account, entry: $0.1, interaction: interaction), directionHint: nil) } - - return UserInfoEntryTransition(deletions: deletions, insertions: insertions, updates: updates) -} - -final class PeerInfoControllerInteraction { - let openSharedMedia: () -> Void - let changeNotificationNoteSettings: () -> Void - - init(openSharedMedia: @escaping () -> Void, changeNotificationNoteSettings: @escaping () -> Void) { - self.openSharedMedia = openSharedMedia - self.changeNotificationNoteSettings = changeNotificationNoteSettings - } -} - -public class UserInfoController: ListController { - private let account: Account - private let peerId: PeerId - - private var _ready = Promise() - override public var ready: Promise { - return self._ready - } - private var didSetReady = false - - private let transitionDisposable = MetaDisposable() - private let changeSettingsDisposable = MetaDisposable() - - public init(account: Account, peerId: PeerId) { - self.account = account - self.peerId = peerId - - super.init() - - self.title = "Info" - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.transitionDisposable.dispose() - self.changeSettingsDisposable.dispose() - } - - override public func displayNodeDidLoad() { - super.displayNodeDidLoad() - - let interaction = PeerInfoControllerInteraction(openSharedMedia: { [weak self] in - if let strongSelf = self { - if let controller = peerSharedMediaController(account: strongSelf.account, peerId: strongSelf.peerId) { - (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) - } - } - }, changeNotificationNoteSettings: { [weak self] in - if let strongSelf = self { - let controller = ActionSheetController() - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - let notificationAction: (Int32) -> Void = { [weak strongSelf] muteUntil in - if let strongSelf = strongSelf { - let muteState: PeerMuteState - if muteUntil <= 0 { - muteState = .unmuted - } else if muteUntil == Int32.max { - muteState = .muted(until: Int32.max) - } else { - muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil) - } - strongSelf.changeSettingsDisposable.set(changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.appDefault)).start()) - } - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Enable", action: { - dismissAction() - notificationAction(0) - }), - ActionSheetButtonItem(title: "Mute for 1 hour", action: { - dismissAction() - notificationAction(1 * 60 * 60) - }), - ActionSheetButtonItem(title: "Mute for 8 hours", action: { - dismissAction() - notificationAction(8 * 60 * 60) - }), - ActionSheetButtonItem(title: "Mute for 2 days", action: { - dismissAction() - notificationAction(2 * 24 * 60 * 60) - }), - ActionSheetButtonItem(title: "Disable", action: { - dismissAction() - notificationAction(Int32.max) - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) - ]) - strongSelf.present(controller, in: .window) - } - }) - - self.listDisplayNode.backgroundColor = UIColor.white - - let previousEntries = Atomic<[UserInfoEntry]?>(value: nil) - - let account = self.account - let transition = userInfoEntries(account: self.account, peerId: self.peerId) - |> map { entries -> (UserInfoEntryTransition, Bool, Bool) in - let previous = previousEntries.swap(entries) - return (preparedUserInfoEntryTransition(account: account, 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) - })) - } - - private func enqueueTransition(_ transition: UserInfoEntryTransition, firstTime: Bool, animated: Bool) { - var options = ListViewDeleteAndInsertOptions() - if firstTime { - options.insert(.Synchronous) - options.insert(.LowLatency) - } else if animated { - options.insert(.AnimateInsertion) - } - self.listDisplayNode.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)) - } - } - }) - } -} diff --git a/TelegramUI/UserInfoEntries.swift b/TelegramUI/UserInfoEntries.swift new file mode 100644 index 0000000000..6af3c984a9 --- /dev/null +++ b/TelegramUI/UserInfoEntries.swift @@ -0,0 +1,255 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +private enum UserInfoSection: UInt32, PeerInfoSection { + case info + case actions + case sharedMediaAndNotifications + case block + + func isEqual(to: PeerInfoSection) -> Bool { + guard let section = to as? UserInfoSection else { + return false + } + return section == self + } + + func isOrderedBefore(_ section: PeerInfoSection) -> Bool { + guard let section = section as? UserInfoSection else { + return false + } + return self.rawValue < section.rawValue + } +} + +enum UserInfoEntry: PeerInfoEntry { + case info(peer: Peer?, cachedData: CachedPeerData?) + case about(text: String) + case phoneNumber(index: Int, value: PhoneNumberWithLabel) + case userName(value: String) + case sendMessage + case shareContact + case startSecretChat + case sharedMedia + case notifications(settings: PeerNotificationSettings?) + case block + + var section: PeerInfoSection { + switch self { + case .info, .about, .phoneNumber, .userName: + return UserInfoSection.info + case .sendMessage, .shareContact, .startSecretChat: + return UserInfoSection.actions + case .sharedMedia, .notifications: + return UserInfoSection.sharedMediaAndNotifications + case .block: + return UserInfoSection.block + } + } + + var stableId: Int { + return self.sortIndex + } + + func isEqual(to: PeerInfoEntry) -> Bool { + guard let entry = to as? UserInfoEntry else { + return false + } + + switch self { + case let .info(lhsPeer, lhsCachedData): + switch entry { + case let .info(rhsPeer, rhsCachedData): + if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhsPeer == nil) != (rhsPeer != nil) { + return false + } + if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { + if !lhsCachedData.isEqual(to: rhsCachedData) { + return false + } + } else if (lhsCachedData != nil) != (rhsCachedData != nil) { + return false + } + return true + default: + return false + } + case let .about(lhsText): + switch entry { + case let .about(lhsText): + return true + default: + return false + } + case let .phoneNumber(lhsIndex, lhsValue): + switch entry { + case let .phoneNumber(rhsIndex, rhsValue) where lhsIndex == rhsIndex && lhsValue == rhsValue: + return true + default: + return false + } + case let .userName(value): + switch entry { + case .userName(value): + return true + default: + return false + } + case .sendMessage: + switch entry { + case .sendMessage: + return true + default: + return false + } + case .shareContact: + switch entry { + case .shareContact: + return true + default: + return false + } + case .startSecretChat: + switch entry { + case .startSecretChat: + return true + default: + return false + } + case .sharedMedia: + switch entry { + case .sharedMedia: + return true + default: + return false + } + case let .notifications(lhsSettings): + switch entry { + case let .notifications(rhsSettings): + if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { + return lhsSettings.isEqual(to: rhsSettings) + } else if (lhsSettings != nil) != (rhsSettings != nil) { + return false + } + return true + default: + return false + } + case .block: + switch entry { + case .block: + return true + default: + return false + } + } + } + + private var sortIndex: Int { + switch self { + case .info: + return 0 + case .about: + return 1 + case let .phoneNumber(index, _): + return 2 + index + case .userName: + return 1000 + case .sendMessage: + return 1001 + case .shareContact: + return 1002 + case .startSecretChat: + return 1003 + case .sharedMedia: + return 1004 + case .notifications: + return 1005 + case .block: + return 1006 + } + } + + func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool { + guard let other = entry as? UserInfoEntry else { + return false + } + + return self.sortIndex < other.sortIndex + } + + func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem { + switch self { + case let .info(peer, cachedData): + return PeerInfoAvatarAndNameItem(account: account, peer: peer, cachedData: cachedData, sectionId: self.section.rawValue, style: .plain) + case let .about(text): + return PeerInfoTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section.rawValue) + case let .phoneNumber(_, value): + return PeerInfoTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section.rawValue) + case let .userName(value): + return PeerInfoTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: self.section.rawValue) + case .sendMessage: + return PeerInfoActionItem(title: "Send Message", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { + + }) + case .shareContact: + return PeerInfoActionItem(title: "Share Contact", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { + + }) + case .startSecretChat: + return PeerInfoActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { + + }) + case .sharedMedia: + return PeerInfoDisclosureItem(title: "Shared Media", label: "", sectionId: self.section.rawValue, style: .plain, action: { + interaction.openSharedMedia() + }) + case let .notifications(settings): + 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: .plain, action: { + interaction.changeNotificationNoteSettings() + }) + case .block: + return PeerInfoActionItem(title: "Block User", kind: .destructive, alignment: .natural, sectionId: self.section.rawValue, style: .plain, action: { + + }) + } + } +} + +func userInfoEntries(view: PeerView) -> [PeerInfoEntry] { + var entries: [PeerInfoEntry] = [] + entries.append(UserInfoEntry.info(peer: view.peers[view.peerId], cachedData: view.cachedData)) + if let cachedUserData = view.cachedData as? CachedUserData { + if let about = cachedUserData.about, !about.isEmpty { + entries.append(UserInfoEntry.about(text: about)) + } + } + if let user = view.peers[view.peerId] as? TelegramUser { + if let phoneNumber = user.phone, !phoneNumber.isEmpty { + entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber))) + } + 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) + entries.append(UserInfoEntry.sharedMedia) + entries.append(UserInfoEntry.notifications(settings: view.notificationSettings)) + entries.append(UserInfoEntry.block) + } + return entries +}