From 580e1cebc5025a75f3a1923e4bcc09d0db529d7f Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 11 Feb 2020 15:42:35 +0100 Subject: [PATCH] Bug fixes and performance improvements --- .../Sources/AccountContext.swift | 2 +- .../Sources/CallListController.swift | 2 +- .../Sources/ContactsController.swift | 2 +- .../Sources/InstantPageControllerNode.swift | 2 +- .../Sources/ItemListPeerItem.swift | 8 +- .../Sources/SecureIdAuthController.swift | 2 +- .../Sources/AvatarGalleryController.swift | 31 +- .../AvatarGalleryItemFooterContentNode.swift | 2 +- .../Sources/PeerAvatarImageGalleryItem.swift | 1 + .../ChannelBannedMemberController.swift | 1 + .../Sources/ChannelBlacklistController.swift | 2 +- .../Sources/ChannelMembersController.swift | 4 +- .../ChannelPermissionsController.swift | 2 +- .../Sources/GroupInfoController.swift | 4 +- .../BlockedPeersController.swift | 2 +- ...ectivePrivacySettingsPeersController.swift | 2 +- .../Sources/ManagedChatListHoles.swift | 2 +- .../TelegramUI/ChatController.swift | 13 +- .../ChatRecentActionsControllerNode.swift | 4 +- .../TelegramUI/OpenAddContact.swift | 2 +- .../TelegramUI/TelegramUI/OpenUrl.swift | 4 +- .../PeerInfoScreenLabeledValueItem.swift | 160 +++++- .../ListItems/PeerInfoScreenMemberItem.swift | 111 +++- ...erInfoScreenSelectableBackgroundNode.swift | 27 +- .../PeerInfo/Panes/PeerInfoMembersPane.swift | 81 ++- .../TelegramUI/PeerInfo/PeerInfoData.swift | 167 +++++- .../PeerInfo/PeerInfoHeaderNode.swift | 32 +- .../TelegramUI/PeerInfo/PeerInfoMembers.swift | 146 ++++- .../PeerInfo/PeerInfoPaneContainerNode.swift | 22 +- .../TelegramUI/PeerInfo/PeerInfoScreen.swift | 499 +++++++++++++++++- .../PeerMediaCollectionController.swift | 2 +- .../TelegramUI/PollResultsController.swift | 2 +- .../TelegramUI/SharedAccountContext.swift | 12 +- .../TelegramUI/TextLinkHandling.swift | 2 +- ...annelMemberCategoriesContextsManager.swift | 171 ++++++ 35 files changed, 1393 insertions(+), 135 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 56df2567ab..297b64c48d 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -442,7 +442,7 @@ public protocol SharedAccountContext: class { func openChatMessage(_ params: OpenChatMessageParams) -> Bool func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, account: Account, chatLocation: ChatLocation, tagMask: MessageTags?) -> Signal<(MessageIndex?, Bool), NoError> func makeOverlayAudioPlayerController(context: AccountContext, peerId: PeerId, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController - func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool) -> ViewController? + func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool) -> ViewController? func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController func makePeersNearbyController(context: AccountContext) -> ViewController func makeComposeController(context: AccountContext) -> ViewController diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 1b87df0834..58e452733f 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -149,7 +149,7 @@ public final class CallListController: ViewController { let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in - if let strongSelf = self, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .calls(messages: messages), avatarInitiallyExpanded: false) { + if let strongSelf = self, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .calls(messages: messages), avatarInitiallyExpanded: false, fromChat: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) } }) diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 087fdfa678..f9f2b5f05e 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -530,7 +530,7 @@ public class ContactsController: ViewController { return } if let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } } else { diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index e15556a53a..1360bc8ac7 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -1180,7 +1180,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self { - if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { strongSelf.getNavigationController()?.pushViewController(controller) } } diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index ce2f1c397a..8c8fcf6b0f 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -208,9 +208,9 @@ private final class LoadingShimmerNode: ASDisplayNode { public struct ItemListPeerItemEditing: Equatable { public var editable: Bool public var editing: Bool - public var revealed: Bool + public var revealed: Bool? - public init(editable: Bool, editing: Bool, revealed: Bool) { + public init(editable: Bool, editing: Bool, revealed: Bool?) { self.editable = editable self.editing = editing self.revealed = revealed @@ -1095,7 +1095,9 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) strongSelf.setRevealOptions((left: [], right: peerRevealOptions)) - strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) + if let revealed = item.editing.revealed { + strongSelf.setRevealOptionsOpened(revealed, animated: animated) + } } }) } diff --git a/submodules/PassportUI/Sources/SecureIdAuthController.swift b/submodules/PassportUI/Sources/SecureIdAuthController.swift index 66ca2894ea..1df3ddb5a0 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthController.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthController.swift @@ -330,7 +330,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable guard let strongSelf = self else { return } - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } }) diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 0692fc4ad2..0d243280fb 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -13,7 +13,7 @@ import GalleryUI public enum AvatarGalleryEntry: Equatable { case topImage([ImageRepresentationWithReference], GalleryItemIndexData?) - case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], Peer, Int32, GalleryItemIndexData?, MessageId?) + case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], Peer?, Int32, GalleryItemIndexData?, MessageId?) public var representations: [ImageRepresentationWithReference] { switch self { @@ -61,7 +61,7 @@ public final class AvatarGalleryControllerPresentationArguments { } } -private func initialAvatarGalleryEntries(peer: Peer) -> [AvatarGalleryEntry] { +public func initialAvatarGalleryEntries(peer: Peer) -> [AvatarGalleryEntry] { var initialEntries: [AvatarGalleryEntry] = [] if !peer.profileImageRepresentations.isEmpty, let peerReference = PeerReference(peer) { initialEntries.append(.topImage(peer.profileImageRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), nil)) @@ -96,6 +96,33 @@ public func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal< ) } +public func fetchedAvatarGalleryEntries(account: Account, peer: Peer, firstEntry: AvatarGalleryEntry) -> Signal<[AvatarGalleryEntry], NoError> { + let initialEntries = [firstEntry] + return Signal<[AvatarGalleryEntry], NoError>.single(initialEntries) + |> then( + requestPeerPhotos(account: account, peerId: peer.id) + |> map { photos -> [AvatarGalleryEntry] in + var result: [AvatarGalleryEntry] = [] + let initialEntries = [firstEntry] + if photos.isEmpty { + result = initialEntries + } else { + var index: Int32 = 0 + for photo in photos { + let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) + if result.isEmpty, let first = initialEntries.first { + result.append(.image(photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) + } else { + result.append(.image(photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) + } + index += 1 + } + } + return result + } + ) +} + public class AvatarGalleryController: ViewController, StandalonePresentableController { private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift index 7132c6cfa2..01cc3e6394 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift @@ -85,7 +85,7 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { var dateText: String? switch entry { case let .image(_, _, peer, date, _, _): - nameText = peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) + nameText = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "" dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date) default: break diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 4789f0c18f..428dcefb09 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift @@ -127,6 +127,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self?._ready.set(.single(Void())) } + self.imageNode.contentAnimations = .subsequentUpdates self.imageNode.view.contentMode = .scaleAspectFill self.imageNode.clipsToBounds = true diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index 4f9e7b9035..6e32a759d5 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -757,6 +757,7 @@ public func channelBannedMemberController(context: AccountContext, peerId: PeerI } let controller = ItemListController(context: context, state: signal) + controller.navigationPresentation = .modal dismissImpl = { [weak controller] in controller?.dismiss() } diff --git a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift index 0750e10edc..f2751edd1b 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift @@ -366,7 +366,7 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) } items.append(ActionSheetButtonItem(title: presentationData.strings.GroupRemoved_ViewUserInfo, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(infoController) } })) diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index dc6dc37ef0..cb1c05e765 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -450,7 +450,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> } })) }, openPeer: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(controller) } }, inviteViaLink: { @@ -502,7 +502,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> return state.withUpdatedSearchingMembers(false) } }, openPeer: { peer, _ in - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(infoController) } }, pushController: { c in diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index 85d9837112..f1813127e9 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -666,7 +666,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) }, openPeerInfo: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(controller) } }, openKicked: { diff --git a/submodules/PeerInfoUI/Sources/GroupInfoController.swift b/submodules/PeerInfoUI/Sources/GroupInfoController.swift index 8e3288a0aa..367096905c 100644 --- a/submodules/PeerInfoUI/Sources/GroupInfoController.swift +++ b/submodules/PeerInfoUI/Sources/GroupInfoController.swift @@ -599,7 +599,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { })) } return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: editing, revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: enabled, selectable: selectable, sectionId: self.section, action: { - if let infoController = arguments.context.sharedContext.makePeerInfoController(context: arguments.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false), selectable { + if let infoController = arguments.context.sharedContext.makePeerInfoController(context: arguments.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false), selectable { arguments.pushController(infoController) } }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in @@ -2342,7 +2342,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: return state.withUpdatedSearchingMembers(false) } }, openPeer: { peer, _ in - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { arguments.pushController(infoController) } }, pushController: { c in diff --git a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift index b59ad0429d..8af1364bf1 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift @@ -259,7 +259,7 @@ public func blockedPeersController(context: AccountContext, blockedPeersContext: } })) }, openPeer: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(controller) } }) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 130a649729..47875e24b9 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -341,7 +341,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri return transaction.getPeer(peerId) } |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) else { + guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) else { return } pushControllerImpl?(controller) diff --git a/submodules/TelegramCore/Sources/ManagedChatListHoles.swift b/submodules/TelegramCore/Sources/ManagedChatListHoles.swift index 4783619e83..ce36e13ef9 100644 --- a/submodules/TelegramCore/Sources/ManagedChatListHoles.swift +++ b/submodules/TelegramCore/Sources/ManagedChatListHoles.swift @@ -59,7 +59,7 @@ func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: Pee let disposable = combineLatest(postbox.chatListHolesView(), topRootHole).start(next: { view, topRootHoleView in var additionalLatestHole: ChatListHole? if let topRootHole = topRootHoleView.views[topRootHoleKey] as? AllChatListHolesView { - additionalLatestHole = topRootHole.latestHole + //additionalLatestHole = topRootHole.latestHole } let (removed, added, addedAdditionalLatestHole) = state.with { state in diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 531666bdfa..6883e15cab 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -309,6 +309,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var isDismissed = false private var focusOnSearchAfterAppearance: Bool = false + + private let keepPeerInfoScreenDataHotDisposable = MetaDisposable() public override var customData: Any? { return self.chatLocation @@ -2513,6 +2515,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.reportIrrelvantGeoDisposable?.dispose() self.reminderActivity?.invalidate() self.updateSlowmodeStatusDisposable.dispose() + self.keepPeerInfoScreenDataHotDisposable.dispose() } public func updatePresentationMode(_ mode: ChatControllerPresentationMode) { @@ -4731,6 +4734,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { })]), in: .window(.root)) })) + + if case let .peer(peerId) = self.chatLocation { + self.keepPeerInfoScreenDataHotDisposable.set(keepPeerInfoScreenDataHot(context: self.context, peerId: peerId).start()) + } } if self.focusOnSearchAfterAppearance { @@ -5356,7 +5363,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if peer.smallProfileImage == nil { expandAvatar = false } - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar, fromChat: true) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } @@ -7113,7 +7120,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar, fromChat: true) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } @@ -7530,7 +7537,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift index c3b087ae3b..521def128a 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift @@ -659,7 +659,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if peer is TelegramChannel, let navigationController = strongSelf.getNavigationController() { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: true)) } else { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { strongSelf.pushController(infoController) } } @@ -681,7 +681,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self { if let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { strongSelf.pushController(infoController) } } diff --git a/submodules/TelegramUI/TelegramUI/OpenAddContact.swift b/submodules/TelegramUI/TelegramUI/OpenAddContact.swift index 98c49ce1ba..dec7f34047 100644 --- a/submodules/TelegramUI/TelegramUI/OpenAddContact.swift +++ b/submodules/TelegramUI/TelegramUI/OpenAddContact.swift @@ -18,7 +18,7 @@ func openAddContactImpl(context: AccountContext, firstName: String = "", lastNam let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: label, value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") present(deviceContactInfoController(context: context, subject: .create(peer: nil, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in if let peer = peer { - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushController(infoController) } } else { diff --git a/submodules/TelegramUI/TelegramUI/OpenUrl.swift b/submodules/TelegramUI/TelegramUI/OpenUrl.swift index 11f7c094e2..37e7801e90 100644 --- a/submodules/TelegramUI/TelegramUI/OpenUrl.swift +++ b/submodules/TelegramUI/TelegramUI/OpenUrl.swift @@ -209,7 +209,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur case .info: let _ = (context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { context.sharedContext.applicationBindings.dismissNativeController() navigationController?.pushViewController(infoController) } @@ -491,7 +491,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: idValue)) } |> deliverOnMainQueue).start(next: { peer in - if let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { navigationController?.pushViewController(controller) } }) diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift index d7f382389f..33a0ebfdac 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -1,6 +1,8 @@ import AsyncDisplayKit import Display import TelegramPresentationData +import AccountContext +import TextFormat enum PeerInfoScreenLabeledValueTextColor { case primary @@ -9,7 +11,7 @@ enum PeerInfoScreenLabeledValueTextColor { enum PeerInfoScreenLabeledValueTextBehavior: Equatable { case singleLine - case multiLine(maxLines: Int) + case multiLine(maxLines: Int, enabledEntities: EnabledEntityTypes) } final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { @@ -19,14 +21,27 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { let textColor: PeerInfoScreenLabeledValueTextColor let textBehavior: PeerInfoScreenLabeledValueTextBehavior let action: (() -> Void)? + let longTapAction: ((ASDisplayNode) -> Void)? + let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? - init(id: AnyHashable, label: String, text: String, textColor: PeerInfoScreenLabeledValueTextColor = .primary, textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine, action: (() -> Void)?) { + init( + id: AnyHashable, + label: String, + text: String, + textColor: PeerInfoScreenLabeledValueTextColor = .primary, + textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine, + action: (() -> Void)?, + longTapAction: ((ASDisplayNode) -> Void)? = nil, + linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil + ) { self.id = id self.label = label self.text = text self.textColor = textColor self.textBehavior = textBehavior self.action = action + self.longTapAction = longTapAction + self.linkItemAction = linkItemAction } func node() -> PeerInfoScreenItemNode { @@ -40,11 +55,15 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { private let textNode: ImmediateTextNode private let bottomSeparatorNode: ASDisplayNode + private var linkHighlightingNode: LinkHighlightingNode? + private var item: PeerInfoScreenLabeledValueItem? + private var theme: PresentationTheme? override init() { var bringToFrontForHighlightImpl: (() -> Void)? self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + self.selectionNode.isUserInteractionEnabled = false self.labelNode = ImmediateTextNode() self.labelNode.displaysAsynchronously = false @@ -69,12 +88,65 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { self.addSubnode(self.textNode) } + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { [weak self] point in + guard let strongSelf = self, let item = strongSelf.item else { + return .keepWithSingleTap + } + if let _ = strongSelf.linkItemAtPoint(point) { + return .waitForSingleTap + } + if item.longTapAction != nil { + return .waitForSingleTap + } + if item.action != nil { + return .keepWithSingleTap + } + return .fail + } + recognizer.highlight = { [weak self] point in + guard let strongSelf = self else { + return + } + strongSelf.updateTouchesAtPoint(point) + } + self.view.addGestureRecognizer(recognizer) + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap, .longTap: + if let item = self.item { + if let linkItem = self.linkItemAtPoint(location) { + item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem) + } else if case .longTap = gesture { + item.longTapAction?(self) + } else if case .tap = gesture { + item.action?() + } + } + default: + break + } + } + default: + break + } + } + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenLabeledValueItem else { return 10.0 } self.item = item + self.theme = presentationData.theme self.selectionNode.pressed = item.action @@ -95,10 +167,25 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { switch item.textBehavior { case .singleLine: self.textNode.maximumNumberOfLines = 1 - case let .multiLine(maxLines): + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) + case let .multiLine(maxLines, enabledEntities): self.textNode.maximumNumberOfLines = maxLines + if enabledEntities.isEmpty { + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) + } else { + let fontSize: CGFloat = 17.0 + + var baseFont = Font.regular(fontSize) + var linkFont = baseFont + var boldFont = Font.medium(fontSize) + var italicFont = Font.italic(fontSize) + var boldItalicFont = Font.semiboldItalic(fontSize) + let titleFixedFont = Font.monospace(fontSize) + + let entities = generateTextEntities(item.text, enabledTypes: enabledEntities) + self.textNode.attributedText = stringWithAppliedEntities(item.text, entities: entities, baseColor: textColorValue, linkColor: presentationData.theme.list.itemAccentColor, baseFont: baseFont, linkFont: linkFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: titleFixedFont, blockQuoteFont: baseFont) + } } - self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) @@ -120,4 +207,69 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { return height } + + private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? { + let textNodeFrame = self.textNode.frame + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + return .url(url) + } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + return .mention(peerName) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + return .hashtag(hashtag.peerName, hashtag.hashtag) + } else { + return nil + } + } + return nil + } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + guard let item = self.item, let theme = self.theme else { + return + } + var rects: [CGRect]? + if let point = point { + let textNodeFrame = self.textNode.frame + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TelegramTextAttributes.URL, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag + ] + for name in possibleNames { + if let _ = attributes[NSAttributedString.Key(rawValue: name)] { + rects = self.textNode.attributeRects(name: name, at: index) + break + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: theme.list.itemAccentColor.withAlphaComponent(0.5)) + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + } + linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + + if point != nil && rects == nil && item.action != nil { + self.selectionNode.updateIsHighlighted(true) + } else { + self.selectionNode.updateIsHighlighted(false) + } + } } diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift index 246861f33d..e593df1e4e 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenMemberItem.swift @@ -11,24 +11,31 @@ import SyncCore import TelegramCore import ItemListUI +enum PeerInfoScreenMemberItemAction { + case open + case promote + case restrict + case remove +} + final class PeerInfoScreenMemberItem: PeerInfoScreenItem { let id: AnyHashable let context: AccountContext - let peer: Peer - let presence: TelegramUserPresence? - let action: (() -> Void)? + let enclosingPeer: Peer + let member: PeerInfoMember + let action: ((PeerInfoScreenMemberItemAction) -> Void)? init( id: AnyHashable, context: AccountContext, - peer: Peer, - presence: TelegramUserPresence?, - action: (() -> Void)? + enclosingPeer: Peer, + member: PeerInfoMember, + action: ((PeerInfoScreenMemberItemAction) -> Void)? ) { self.id = id self.context = context - self.peer = peer - self.presence = presence + self.enclosingPeer = enclosingPeer + self.member = member self.action = action } @@ -47,6 +54,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { override init() { var bringToFrontForHighlightImpl: (() -> Void)? self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + self.selectionNode.isUserInteractionEnabled = false self.bottomSeparatorNode = ASDisplayNode() self.bottomSeparatorNode.isLayerBacked = true @@ -61,6 +69,40 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { self.addSubnode(self.selectionNode) } + override func didLoad() { + super.didLoad() + + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.tapActionAtPoint = { [weak self] point in + return .keepWithSingleTap + } + recognizer.highlight = { [weak self] point in + guard let strongSelf = self else { + return + } + strongSelf.updateTouchesAtPoint(point) + } + self.view.addGestureRecognizer(recognizer) + } + + @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { + switch recognizer.state { + case .ended: + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let item = self.item { + item.action?(.open) + } + default: + break + } + } + default: + break + } + } + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenMemberItem else { return 10.0 @@ -68,13 +110,50 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { self.item = item - self.selectionNode.pressed = item.action + self.selectionNode.pressed = item.action.flatMap { action in + return { + action(.open) + } + } let sideInset: CGFloat = 16.0 self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: item.peer, height: .peerList, presence: item.presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in + let label: String? + if let rank = item.member.rank { + label = rank + } else { + switch item.member.role { + case .creator: + label = presentationData.strings.GroupInfo_LabelOwner + case .admin: + label = presentationData.strings.GroupInfo_LabelAdmin + case .member: + label = nil + } + } + + let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.account.peerId, peer: item.enclosingPeer, member: item.member) + + var options: [ItemListPeerItemRevealOption] = [] + if actions.contains(.promote) && item.enclosingPeer is TelegramChannel { + options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: { + item.action?(.promote) + })) + } + if actions.contains(.restrict) { + if item.enclosingPeer is TelegramChannel { + options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: { + item.action?(.restrict) + })) + } + options.append(ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: { + item.action?(.remove) + })) + } + + let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: item.member.peer, height: .peerList, presence: item.member.presence, text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: nil), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: false, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in }, removePeer: { _ in @@ -103,7 +182,6 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { apply().1(ListViewItemApply(isOnScreen: true)) }) itemNode = itemNodeValue as! ItemListPeerItemNode - itemNode.isUserInteractionEnabled = false self.itemNode = itemNode self.addSubnode(itemNode) } @@ -121,4 +199,15 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { return height } + + private func updateTouchesAtPoint(_ point: CGPoint?) { + guard let item = self.item else { + return + } + if point != nil && item.context.account.peerId != item.member.id { + self.selectionNode.updateIsHighlighted(true) + } else { + self.selectionNode.updateIsHighlighted(false) + } + } } diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSelectableBackgroundNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSelectableBackgroundNode.swift index 08b0c6e17a..a630c37287 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSelectableBackgroundNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSelectableBackgroundNode.swift @@ -8,6 +8,8 @@ final class PeerInfoScreenSelectableBackgroundNode: ASDisplayNode { let bringToFrontForHighlight: () -> Void + private var isHighlighted: Bool = false + var pressed: (() -> Void)? { didSet { self.buttonNode.isUserInteractionEnabled = self.pressed != nil @@ -30,16 +32,7 @@ final class PeerInfoScreenSelectableBackgroundNode: ASDisplayNode { self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.buttonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.bringToFrontForHighlight() - strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.backgroundNode.alpha = 1.0 - } else { - strongSelf.backgroundNode.alpha = 0.0 - strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) - } - } + self?.updateIsHighlighted(highlighted) } } @@ -47,6 +40,20 @@ final class PeerInfoScreenSelectableBackgroundNode: ASDisplayNode { self.pressed?() } + func updateIsHighlighted(_ isHighlighted: Bool) { + if self.isHighlighted != isHighlighted { + self.isHighlighted = isHighlighted + if isHighlighted { + self.bringToFrontForHighlight() + self.backgroundNode.layer.removeAnimation(forKey: "opacity") + self.backgroundNode.alpha = 1.0 + } else { + self.backgroundNode.alpha = 0.0 + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + } + func update(size: CGSize, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { self.backgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift index 243a95f6a9..d555c930a0 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoMembersPane.swift @@ -19,6 +19,13 @@ private struct PeerMembersListTransaction { let updates: [ListViewUpdateItem] } +enum PeerMembersListAction { + case open + case promote + case restrict + case remove +} + private struct PeerMembersListEntry: Comparable, Identifiable { var index: Int var member: PeerInfoMember @@ -35,10 +42,43 @@ private struct PeerMembersListEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) -> ListViewItem { let member = self.member - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: member.peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { - openPeer(member.peer) + let label: String? + if let rank = member.rank { + label = rank + } else { + switch member.role { + case .creator: + label = presentationData.strings.GroupInfo_LabelOwner + case .admin: + label = presentationData.strings.GroupInfo_LabelAdmin + case .member: + label = nil + } + } + + let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member) + + var options: [ItemListPeerItemRevealOption] = [] + if actions.contains(.promote) && enclosingPeer is TelegramChannel{ + options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: { + action(member, .promote) + })) + } + if actions.contains(.restrict) { + if enclosingPeer is TelegramChannel { + options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: { + action(member, .restrict) + })) + } + options.append(ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: { + action(member, .remove) + })) + } + + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: member.peer, presence: member.presence, text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: false), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: member.id != context.account.peerId, sectionId: 0, action: { + action(member, .open) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, contextAction: nil/*{ node, gesture in @@ -47,12 +87,12 @@ private struct PeerMembersListEntry: Comparable, Identifiable { } } -private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void) -> PeerMembersListTransaction { +private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) -> PeerMembersListTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, action: action), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enclosingPeer: enclosingPeer, action: action), directionHint: nil) } return PeerMembersListTransaction(deletions: deletions, insertions: insertions, updates: updates) } @@ -60,9 +100,11 @@ private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toE final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext private let membersContext: PeerInfoMembersContext + private let action: (PeerInfoMember, PeerMembersListAction) -> Void private let listNode: ListView private var currentEntries: [PeerMembersListEntry] = [] + private var enclosingPeer: Peer? private var currentState: PeerInfoMembersState? private var canLoadMore: Bool = false private var enqueuedTransactions: [PeerMembersListTransaction] = [] @@ -77,9 +119,10 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { private var disposable: Disposable? - init(context: AccountContext, membersContext: PeerInfoMembersContext) { + init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) { self.context = context self.membersContext = membersContext + self.action = action self.listNode = ListView() @@ -88,14 +131,19 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { self.listNode.preloadPages = true self.addSubnode(self.listNode) - self.disposable = (membersContext.state - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { + self.disposable = (combineLatest(queue: .mainQueue(), + membersContext.state, + context.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + ) + |> deliverOnMainQueue).start(next: { [weak self] state, combinedView in + guard let strongSelf = self, let basicPeerView = combinedView.views[.basicPeer(peerId)] as? BasicPeerView, let enclosingPeer = basicPeerView.peer else { return } + + strongSelf.enclosingPeer = enclosingPeer strongSelf.currentState = state if let (_, _, presentationData) = strongSelf.currentParams { - strongSelf.updateState(state: state, presentationData: presentationData) + strongSelf.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData) } }) @@ -132,19 +180,20 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { self.listNode.scrollEnabled = !isScrollingLockedAtTop - if isFirstLayout, let state = self.currentState { - self.updateState(state: state, presentationData: presentationData) + if isFirstLayout, let enclosingPeer = self.enclosingPeer, let state = self.currentState { + self.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData) } } - private func updateState(state: PeerInfoMembersState, presentationData: PresentationData) { + private func updateState(enclosingPeer: Peer, state: PeerInfoMembersState, presentationData: PresentationData) { var entries: [PeerMembersListEntry] = [] for member in state.members { entries.append(PeerMembersListEntry(index: entries.count, member: member)) } - let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in - + let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, enclosingPeer: enclosingPeer, action: { [weak self] member, action in + self?.action(member, action) }) + self.enclosingPeer = enclosingPeer self.currentEntries = entries self.enqueuedTransactions.append(transaction) self.dequeueTransaction() diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift index 09bbdafd90..a2a81d49fe 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift @@ -8,6 +8,7 @@ import AccountContext import PeerPresenceStatusManager import TelegramStringFormatting import TelegramPresentationData +import PeerAvatarGalleryUI enum PeerInfoUpdatingAvatar { case none @@ -91,17 +92,17 @@ final class PeerInfoScreenData { } } -enum PeerInfoScreenInputUserKind { +private enum PeerInfoScreenInputUserKind { case user case bot case support } -enum PeerInfoScreenInputData: Equatable { +private enum PeerInfoScreenInputData: Equatable { case none case user(userId: PeerId, secretChatId: PeerId?, kind: PeerInfoScreenInputUserKind) case channel - case group(isSupergroup: Bool, membersContext: PeerInfoMembersContext) + case group(groupId: PeerId) } func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId) -> Signal<[PeerInfoPaneKey], NoError> { @@ -148,11 +149,20 @@ struct PeerInfoStatusData: Equatable { } enum PeerInfoMembersData: Equatable { - case shortList([PeerInfoMember]) + case shortList(membersContext: PeerInfoMembersContext, members: [PeerInfoMember]) case longList(PeerInfoMembersContext) + + var membersContext: PeerInfoMembersContext { + switch self { + case let .shortList(shortList): + return shortList.membersContext + case let .longList(membersContext): + return membersContext + } + } } -func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> Signal { +private func peerInfoScreenInputData(context: AccountContext, peerId: PeerId) -> Signal { return context.account.postbox.combinedView(keys: [.basicPeer(peerId)]) |> map { view -> PeerInfoScreenInputData in guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else { @@ -170,17 +180,68 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen return .user(userId: user.id, secretChatId: nil, kind: kind) } else if let channel = peer as? TelegramChannel { if case .group = channel.info { - return .group(isSupergroup: true, membersContext: PeerInfoMembersContext(context: context, peerId: channel.id)) + return .group(groupId: channel.id) } else { return .channel } } else if let group = peer as? TelegramGroup { - return .group(isSupergroup: false, membersContext: PeerInfoMembersContext(context: context, peerId: group.id)) + return .group(groupId: group.id) } else { return .none } } |> distinctUntilChanged +} + +private func peerInfoProfilePhotos(context: AccountContext, peerId: PeerId) -> Signal { + return context.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + |> map { view -> AvatarGalleryEntry? in + guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else { + return nil + } + return initialAvatarGalleryEntries(peer: peer).first + } + |> distinctUntilChanged + |> mapToSignal { firstEntry -> Signal<[AvatarGalleryEntry], NoError> in + if let firstEntry = firstEntry { + return context.account.postbox.loadedPeerWithId(peerId) + |> mapToSignal { peer -> Signal<[AvatarGalleryEntry], NoError>in + return fetchedAvatarGalleryEntries(account: context.account, peer: peer, firstEntry: firstEntry) + } + } else { + return .single([]) + } + } + |> map { items -> Any in + return items + } +} + +func peerInfoProfilePhotosWithCache(context: AccountContext, peerId: PeerId) -> Signal<[AvatarGalleryEntry], NoError> { + return context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) + |> map { items -> [AvatarGalleryEntry] in + return items as? [AvatarGalleryEntry] ?? [] + } +} + +func keepPeerInfoScreenDataHot(context: AccountContext, peerId: PeerId) -> Signal { + return peerInfoScreenInputData(context: context, peerId: peerId) + |> mapToSignal { inputData -> Signal in + switch inputData { + case .none: + return .complete() + case .user, .channel, .group: + return combineLatest( + context.peerChannelMemberCategoriesContextsManager.profileData(postbox: context.account.postbox, network: context.account.network, peerId: peerId, customData: peerInfoAvailableMediaPanes(context: context, peerId: peerId) |> ignoreValues), + context.peerChannelMemberCategoriesContextsManager.profilePhotos(postbox: context.account.postbox, network: context.account.network, peerId: peerId, fetch: peerInfoProfilePhotos(context: context, peerId: peerId)) |> ignoreValues + ) + |> ignoreValues + } + } +} + +func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> Signal { + return peerInfoScreenInputData(context: context, peerId: peerId) |> mapToSignal { inputData -> Signal in switch inputData { case .none: @@ -382,10 +443,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen members: nil ) } - case let .group(_, membersContext): - let status = context.account.viewTracker.peerView(peerId, updateData: false) + case let .group(groupId): + let status = context.account.viewTracker.peerView(groupId, updateData: false) |> map { peerView -> PeerInfoStatusData? in - guard let channel = peerView.peers[peerId] as? TelegramChannel else { + guard let channel = peerView.peers[groupId] as? TelegramChannel else { return PeerInfoStatusData(text: strings.Channel_Status, isActivity: false) } if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount, memberCount != 0 { @@ -396,12 +457,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } |> distinctUntilChanged + let membersContext = PeerInfoMembersContext(context: context, peerId: groupId) + let membersData: Signal = membersContext.state |> map { state -> PeerInfoMembersData? in if state.members.count > 5 { return .longList(membersContext) } else { - return .shortList(state.members) + return .shortList(membersContext: membersContext, members: state.members) } } |> distinctUntilChanged @@ -409,9 +472,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) var combinedKeys: [PostboxViewKey] = [] combinedKeys.append(globalNotificationsKey) - return combineLatest( - context.account.viewTracker.peerView(peerId, updateData: true), - peerInfoAvailableMediaPanes(context: context, peerId: peerId), + return combineLatest(queue: .mainQueue(), + context.account.viewTracker.peerView(groupId, updateData: true), + peerInfoAvailableMediaPanes(context: context, peerId: groupId), context.account.postbox.combinedView(keys: combinedKeys), status, membersData @@ -435,7 +498,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } return PeerInfoScreenData( - peer: peerView.peers[peerId], + peer: peerView.peers[groupId], cachedData: peerView.cachedData, status: status, notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, @@ -470,6 +533,80 @@ func canEditPeerInfo(peer: Peer?) -> Bool { return false } +struct PeerInfoMemberActions: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let restrict = PeerInfoMemberActions(rawValue: 1 << 0) + static let promote = PeerInfoMemberActions(rawValue: 1 << 1) +} + +func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer, member: PeerInfoMember) -> PeerInfoMemberActions { + var result: PeerInfoMemberActions = [] + + if member.id != accountPeerId { + if let channel = peer as? TelegramChannel { + if channel.flags.contains(.isCreator) { + result.insert(.restrict) + result.insert(.promote) + } else { + switch member { + case let .channelMember(channelMember): + switch channelMember.participant { + case .creator: + break + case let .member(member): + if let adminInfo = member.adminInfo { + if adminInfo.promotedBy == accountPeerId { + result.insert(.restrict) + if channel.hasPermission(.addAdmins) { + result.insert(.promote) + } + } + } else { + if channel.hasPermission(.banMembers) { + result.insert(.restrict) + } + } + } + case .legacyGroupMember: + break + } + } + } else if let group = peer as? TelegramGroup { + switch group.role { + case .creator: + result.insert(.restrict) + result.insert(.promote) + case .admin: + switch member { + case let .legacyGroupMember(legacyGroupMember): + if legacyGroupMember.invitedBy == accountPeerId { + result.insert(.restrict) + result.insert(.promote) + } + case .channelMember: + break + } + case .member: + switch member { + case let .legacyGroupMember(legacyGroupMember): + if legacyGroupMember.invitedBy == accountPeerId { + result.insert(.restrict) + } + case .channelMember: + break + } + } + } + } + + return result +} + func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?) -> [PeerInfoHeaderButtonKey] { var result: [PeerInfoHeaderButtonKey] = [] if let user = peer as? TelegramUser { diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift index 606947df54..ff3b630069 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift @@ -168,6 +168,7 @@ final class PeerInfoAvatarListItemNode: ASDisplayNode { super.init() + self.imageNode.contentAnimations = .subsequentUpdates self.addSubnode(self.imageNode) self.imageNode.imageUpdated = { [weak self] _ in @@ -420,7 +421,7 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { self.updateItems(size: size, transition: .immediate) } else if self.items.count > 1 { self.currentIndex = self.items.count - 1 - self.updateItems(size: size, transition: .immediate) + self.updateItems(size: size, transition: .immediate, synchronous: true) } } else if location.x > size.width * 4.0 / 5.0 { if self.currentIndex < self.items.count - 1 { @@ -486,7 +487,7 @@ final class PeerInfoAvatarListContainerNode: ASDisplayNode { if let peer = peer, !self.initializedList { self.initializedList = true - self.disposable.set((fetchedAvatarGalleryEntries(account: self.context.account, peer: peer) + self.disposable.set((peerInfoProfilePhotosWithCache(context: self.context, peerId: peer.id) |> deliverOnMainQueue).start(next: { [weak self] entries in guard let strongSelf = self else { return @@ -1225,6 +1226,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var context: AccountContext private var presentationData: PresentationData? + private let keepExpandedButtons: PeerInfoScreenKeepExpandedButtons + private(set) var isAvatarExpanded: Bool let avatarListNode: PeerInfoAvatarListNode @@ -1250,9 +1253,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { var navigationTransition: PeerInfoHeaderNavigationTransition? - init(context: AccountContext, avatarInitiallyExpanded: Bool) { + init(context: AccountContext, avatarInitiallyExpanded: Bool, keepExpandedButtons: PeerInfoScreenKeepExpandedButtons) { self.context = context self.isAvatarExpanded = avatarInitiallyExpanded + self.keepExpandedButtons = keepExpandedButtons self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded) @@ -1528,7 +1532,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsContainerNode, frame: CGRect(origin: CGPoint(x: -controlsClippingFrame.minX, y: -controlsClippingFrame.minY), size: CGSize(width: expandedAvatarListSize.width, height: expandedAvatarListSize.height))) transition.updateFrame(node: self.avatarListNode.listContainerNode.shadowNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: expandedAvatarListSize.width, height: navigationHeight + 20.0))) - transition.updateFrame(node: self.avatarListNode.listContainerNode.stripContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusBarHeight + 2.0), size: CGSize(width: expandedAvatarListSize.width, height: 2.0))) + transition.updateFrame(node: self.avatarListNode.listContainerNode.stripContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusBarHeight < 25.0 ? (statusBarHeight + 2.0) : (statusBarHeight - 3.0)), size: CGSize(width: expandedAvatarListSize.width, height: 2.0))) transition.updateFrame(node: self.avatarListNode.listContainerNode.highlightContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: expandedAvatarListSize.width, height: expandedAvatarListSize.height))) transition.updateAlpha(node: self.avatarListNode.listContainerNode.controlsContainerNode, alpha: self.isAvatarExpanded ? (1.0 - transitionFraction) : 0.0) @@ -1690,7 +1694,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { buttonText = "Message" buttonIcon = .message case .discussion: - buttonText = "Discussion" + buttonText = "Discuss" buttonIcon = .message case .call: buttonText = "Call" @@ -1716,11 +1720,21 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateAlpha(node: buttonNode, alpha: buttonsAlpha) let hiddenWhileExpanded: Bool - switch buttonKey { + switch self.keepExpandedButtons { + case .message: + switch buttonKey { + case .mute, .addMember: + hiddenWhileExpanded = true + default: + hiddenWhileExpanded = false + } case .mute: - hiddenWhileExpanded = true - default: - hiddenWhileExpanded = false + switch buttonKey { + case .message, .addMember: + hiddenWhileExpanded = true + default: + hiddenWhileExpanded = false + } } if self.isAvatarExpanded, hiddenWhileExpanded { diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoMembers.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoMembers.swift index ecc921a2c0..cbfd8230b7 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoMembers.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoMembers.swift @@ -6,20 +6,31 @@ import TelegramCore import AccountContext import TemporaryCachedPeerDataManager +enum PeerInfoMemberRole { + case creator + case admin + case member +} + enum PeerInfoMember: Equatable { case channelMember(RenderedChannelParticipant) + case legacyGroupMember(peer: RenderedPeer, role: PeerInfoMemberRole, invitedBy: PeerId?, presence: TelegramUserPresence?) var id: PeerId { switch self { case let .channelMember(channelMember): return channelMember.peer.id + case let .legacyGroupMember(legacyGroupMember): + return legacyGroupMember.peer.peerId } } var peer: Peer { switch self { - case let .channelMember(channelMember): + case let .channelMember(channelMember): return channelMember.peer + case let .legacyGroupMember(legacyGroupMember): + return legacyGroupMember.peer.peers[legacyGroupMember.peer.peerId]! } } @@ -27,6 +38,40 @@ enum PeerInfoMember: Equatable { switch self { case let .channelMember(channelMember): return channelMember.presences[channelMember.peer.id] as? TelegramUserPresence + case let .legacyGroupMember(legacyGroupMember): + return legacyGroupMember.presence + } + } + + var role: PeerInfoMemberRole { + switch self { + case let .channelMember(channelMember): + switch channelMember.participant { + case .creator: + return .creator + case let .member(member): + if member.adminInfo != nil { + return .admin + } else { + return .member + } + } + case let .legacyGroupMember(legacyGroupMember): + return legacyGroupMember.role + } + } + + var rank: String? { + switch self { + case let .channelMember(channelMember): + switch channelMember.participant { + case let .creator(creator): + return creator.rank + case let .member(member): + return member.rank + } + case .legacyGroupMember: + return nil } } } @@ -41,6 +86,31 @@ struct PeerInfoMembersState: Equatable { var dataState: PeerInfoMembersDataState } +private func membersSortedByPresence(_ members: [PeerInfoMember], accountPeerId: PeerId) -> [PeerInfoMember] { + return members.sorted(by: { lhs, rhs in + if lhs.id == accountPeerId { + return true + } else if rhs.id == accountPeerId { + return false + } + + let lhsPresence = lhs.presence + let rhsPresence = rhs.presence + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + if lhsPresence.status < rhsPresence.status { + return false + } else if lhsPresence.status > rhsPresence.status { + return true + } + } else if let _ = lhsPresence { + return true + } else if let _ = rhsPresence { + return false + } + return lhs.id < rhs.id + }) +} + private final class PeerInfoMembersContextImpl { private let queue: Queue private let context: AccountContext @@ -48,6 +118,7 @@ private final class PeerInfoMembersContextImpl { private var members: [PeerInfoMember] = [] private var dataState: PeerInfoMembersDataState = .loading(isInitial: true) + private var removingMemberIds: [PeerId: Disposable] = [:] private let stateValue = Promise() var state: Signal { @@ -70,7 +141,14 @@ private final class PeerInfoMembersContextImpl { guard let strongSelf = self else { return } - strongSelf.members = state.list.map(PeerInfoMember.channelMember) + let unsortedMembers = state.list.map(PeerInfoMember.channelMember) + let members: [PeerInfoMember] + if unsortedMembers.count <= 50 { + members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId) + } else { + members = unsortedMembers + } + strongSelf.members = members switch state.loadingState { case let .loading(initial): strongSelf.dataState = .loading(isInitial: initial) @@ -88,12 +166,26 @@ private final class PeerInfoMembersContextImpl { guard let strongSelf = self, let cachedData = view.cachedData as? CachedGroupData, let participantsData = cachedData.participants else { return } - var members: [PeerInfoMember] = [] + var unsortedMembers: [PeerInfoMember] = [] for participant in participantsData.participants { if let peer = view.peers[participant.peerId] { - + let role: PeerInfoMemberRole + let invitedBy: PeerId? + switch participant { + case .creator: + role = .creator + invitedBy = nil + case let .admin(admin): + role = .admin + invitedBy = admin.invitedBy + case let .member(member): + role = .member + invitedBy = member.invitedBy + } + unsortedMembers.append(.legacyGroupMember(peer: RenderedPeer(peer: peer), role: role, invitedBy: invitedBy, presence: view.peerPresences[participant.peerId] as? TelegramUserPresence)) } } + strongSelf.members = membersSortedByPresence(unsortedMembers, accountPeerId: strongSelf.context.account.peerId) strongSelf.dataState = .ready(canLoadMore: false) strongSelf.pushState() })) @@ -108,7 +200,13 @@ private final class PeerInfoMembersContextImpl { } private func pushState() { - self.stateValue.set(.single(PeerInfoMembersState(members: self.members, dataState: self.dataState))) + if self.removingMemberIds.isEmpty { + self.stateValue.set(.single(PeerInfoMembersState(members: self.members, dataState: self.dataState))) + } else { + self.stateValue.set(.single(PeerInfoMembersState(members: self.members.filter { member in + return self.removingMemberIds[member.id] == nil + }, dataState: self.dataState))) + } } func loadMore() { @@ -116,6 +214,38 @@ private final class PeerInfoMembersContextImpl { self.context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: self.peerId, control: channelMembersControl) } } + + func removeMember(memberId: PeerId) { + if removingMemberIds[memberId] == nil { + let signal: Signal + if self.peerId.namespace == Namespaces.Peer.CloudChannel { + signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: self.context.account, peerId: self.peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + |> ignoreValues + } else { + signal = removePeerMember(account: self.context.account, peerId: self.peerId, memberId: memberId) + |> ignoreValues + } + let completed: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + if let _ = strongSelf.removingMemberIds.removeValue(forKey: memberId) { + strongSelf.pushState() + } + } + let disposable = MetaDisposable() + self.removingMemberIds[memberId] = disposable + + self.pushState() + + disposable.set((signal + |> deliverOn(self.queue)).start(error: { _ in + completed() + }, completed: { + completed() + })) + } + } } final class PeerInfoMembersContext: Equatable { @@ -147,6 +277,12 @@ final class PeerInfoMembersContext: Equatable { } } + func removeMember(memberId: PeerId) { + self.impl.with { impl in + impl.removeMember(memberId: memberId) + } + } + static func ==(lhs: PeerInfoMembersContext, rhs: PeerInfoMembersContext) -> Bool { return lhs === rhs } diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift index 9eb080c548..97da970c3b 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift @@ -60,6 +60,8 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { private let titleNode: ImmediateTextNode private let buttonNode: HighlightTrackingButtonNode + private var isSelected: Bool = false + init(pressed: @escaping () -> Void) { self.pressed = pressed @@ -74,9 +76,9 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { self.addSubnode(self.buttonNode) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - self.buttonNode.highligthedChanged = { [weak self] highlighted in + /*self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { - if highlighted { + if highlighted && !strongSelf.isSelected { strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode.alpha = 0.4 } else { @@ -84,7 +86,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } - } + }*/ } @objc private func buttonPressed() { @@ -92,6 +94,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { } func updateText(_ title: String, isSelected: Bool, presentationData: PresentationData) { + self.isSelected = isSelected self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) } @@ -304,8 +307,10 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { var chatControllerInteraction: ChatControllerInteraction? var openPeerContextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? + var requestPerformPeerMemberAction: ((PeerInfoMember, PeerMembersListAction) -> Void)? var currentPaneUpdated: (() -> Void)? + var requestExpandTabs: (() -> Bool)? private var currentAvailablePanes: [PeerInfoPaneKey]? @@ -336,7 +341,10 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { return } if strongSelf.currentPaneKey == key { - strongSelf.currentPane?.node.scrollToTop() + if let requestExpandTabs = strongSelf.requestExpandTabs, requestExpandTabs() { + } else { + strongSelf.currentPane?.node.scrollToTop() + } return } if strongSelf.currentCandidatePaneKey == key { @@ -449,10 +457,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode { case .music: paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .music) case .groupsInCommon: - paneNode = PeerInfoGroupsInCommonPaneNode(context: self.context, peerId: peerId, chatControllerInteraction: self.chatControllerInteraction!, openPeerContextAction: self.openPeerContextAction!, peers: data?.groupsInCommon ?? []) + paneNode = PeerInfoGroupsInCommonPaneNode(context: self.context, peerId: self.peerId, chatControllerInteraction: self.chatControllerInteraction!, openPeerContextAction: self.openPeerContextAction!, peers: data?.groupsInCommon ?? []) case .members: if case let .longList(membersContext) = data?.members { - paneNode = PeerInfoMembersPaneNode(context: self.context, membersContext: membersContext) + paneNode = PeerInfoMembersPaneNode(context: self.context, peerId: self.peerId, membersContext: membersContext, action: { [weak self] member, action in + self?.requestPerformPeerMemberAction?(member, action) + }) } else { preconditionFailure() } diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift index 346f706c01..aa8dec56dd 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift @@ -35,6 +35,7 @@ import WebSearchUI import LocationResources import LocationUI import Geocoding +import TextFormat protocol PeerInfoScreenItem: class { var id: AnyHashable { get } @@ -53,6 +54,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { private let backgroundNode: ASDisplayNode private let topSeparatorNode: ASDisplayNode private let bottomSeparatorNode: ASDisplayNode + private let itemContainerNode: ASDisplayNode private var currentItems: [PeerInfoScreenItem] = [] private var itemNodes: [AnyHashable: PeerInfoScreenItemNode] = [:] @@ -67,9 +69,13 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { self.bottomSeparatorNode = ASDisplayNode() self.bottomSeparatorNode.isLayerBacked = true + self.itemContainerNode = ASDisplayNode() + self.itemContainerNode.clipsToBounds = true + super.init() self.addSubnode(self.backgroundNode) + self.addSubnode(self.itemContainerNode) self.addSubnode(self.topSeparatorNode) self.addSubnode(self.bottomSeparatorNode) } @@ -94,7 +100,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { wasAdded = true itemNode = item.node() self.itemNodes[item.id] = itemNode - self.addSubnode(itemNode) + self.itemContainerNode.addSubnode(itemNode) itemNode.bringToFrontForHighlight = { [weak self, weak itemNode] in guard let strongSelf = self, let itemNode = itemNode else { return @@ -150,12 +156,14 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { } for id in removeIds { if let itemNode = self.itemNodes.removeValue(forKey: id) { + itemNode.view.superview?.sendSubviewToBack(itemNode.view) transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in itemNode?.removeFromSupernode() }) } } + transition.updateFrame(node: self.itemContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentHeight))) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundOffset), size: CGSize(width: width, height: max(0.0, contentWithBackgroundHeight - contentWithBackgroundOffset)))) transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundOffset - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentWithBackgroundHeight), size: CGSize(width: width, height: UIScreenPixel))) @@ -445,6 +453,18 @@ private enum PeerInfoParticipantsSection { case banned } +private enum PeerInfoMemberAction { + case promote + case restrict + case remove +} + +private enum PeerInfoContextSubject { + case bio + case phone(String) + case link +} + private final class PeerInfoInteraction { let openUsername: (String) -> Void let openPhone: (String) -> Void @@ -468,6 +488,9 @@ private final class PeerInfoInteraction { let openLocation: () -> Void let editingOpenSetupLocation: () -> Void let openPeerInfo: (Peer) -> Void + let performMemberAction: (PeerInfoMember, PeerInfoMemberAction) -> Void + let openPeerInfoContextMenu: (PeerInfoContextSubject, ASDisplayNode) -> Void + let performBioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void init( openUsername: @escaping (String) -> Void, @@ -491,7 +514,10 @@ private final class PeerInfoInteraction { editingOpenStickerPackSetup: @escaping () -> Void, openLocation: @escaping () -> Void, editingOpenSetupLocation: @escaping () -> Void, - openPeerInfo: @escaping (Peer) -> Void + openPeerInfo: @escaping (Peer) -> Void, + performMemberAction: @escaping (PeerInfoMember, PeerInfoMemberAction) -> Void, + openPeerInfoContextMenu: @escaping (PeerInfoContextSubject, ASDisplayNode) -> Void, + performBioLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void ) { self.openUsername = openUsername self.openPhone = openPhone @@ -515,9 +541,14 @@ private final class PeerInfoInteraction { self.openLocation = openLocation self.editingOpenSetupLocation = editingOpenSetupLocation self.openPeerInfo = openPeerInfo + self.performMemberAction = performMemberAction + self.openPeerInfoContextMenu = openPeerInfoContextMenu + self.performBioLinkAction = performBioLinkAction } } +private let enabledBioEntities: EnabledEntityTypes = [.url, .mention, .hashtag] + private func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [(AnyHashable, [PeerInfoScreenItem])] { guard let data = data else { return [] @@ -534,22 +565,34 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese items[section] = [] } + let bioContextAction: (ASDisplayNode) -> Void = { sourceNode in + interaction.openPeerInfoContextMenu(.bio, sourceNode) + } + let bioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void = { action, item in + interaction.performBioLinkAction(action, item) + } + if let user = data.peer as? TelegramUser { if let phone = user.phone { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: "mobile", text: "\(formatPhoneNumber(phone))", textColor: .accent, action: { + let formattedPhone = formatPhoneNumber(phone) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: "mobile", text: formattedPhone, textColor: .accent, action: { interaction.openPhone(phone) + }, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.phone(formattedPhone), sourceNode) })) } if let username = user.username { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 1, label: "username", text: "@\(username)", textColor: .accent, action: { interaction.openUsername(username) + }, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.link, sourceNode) })) } if let cachedData = data.cachedData as? CachedUserData { if user.isScam { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledBioEntities : []), action: nil)) } else if let about = cachedData.about, !about.isEmpty { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction)) } } if !data.isContact { @@ -606,13 +649,15 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if let username = channel.username { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemUsername, label: presentationData.strings.Channel_LinkItem, text: "https://t.me/\(username)", textColor: .accent, action: { interaction.openUsername(username) + }, longTapAction: { sourceNode in + interaction.openPeerInfoContextMenu(.link, sourceNode) })) } if let cachedData = data.cachedData as? CachedChannelData { if channel.isScam { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Profile_About, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Profile_About, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil)) } else if let about = cachedData.about, !about.isEmpty { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Profile_About, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Profile_About, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction)) } if case .broadcast = channel.info { @@ -642,21 +687,31 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else if let group = data.peer as? TelegramGroup { if let cachedData = data.cachedData as? CachedGroupData { if group.isScam { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil)) } else if let about = cachedData.about, !about.isEmpty { - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil)) + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Profile_About, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction)) } } } - if let members = data.members, case let .shortList(memberList) = members { + if let peer = data.peer, let members = data.members, case let .shortList(_, memberList) = members { for member in memberList { var presence = member.presence - if member.id == context.account.peerId { + let isAccountPeer = member.id == context.account.peerId + if isAccountPeer { presence = TelegramUserPresence(status: .present(until: Int32.max - 1), lastActivity: 0) } - items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: context, peer: member.peer, presence: presence, action: { - interaction.openPeerInfo(member.peer) + items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: context, enclosingPeer: peer, member: member, action: isAccountPeer ? nil : { action in + switch action { + case .open: + interaction.openPeerInfo(member.peer) + case .promote: + interaction.performMemberAction(member, .promote) + case .restrict: + interaction.performMemberAction(member, .restrict) + case .remove: + interaction.performMemberAction(member, .remove) + } })) } } @@ -967,6 +1022,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private let activeActionDisposable = MetaDisposable() private let resolveUrlDisposable = MetaDisposable() private let toggleShouldChannelMessagesSignaturesDisposable = MetaDisposable() + private let selectAddMemberDisposable = MetaDisposable() + private let addMemberDisposable = MetaDisposable() private let updateAvatarDisposable = MetaDisposable() private let currentAvatarMixin = Atomic(value: nil) @@ -977,7 +1034,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } private var didSetReady = false - init(controller: PeerInfoScreen, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool) { + init(controller: PeerInfoScreen, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, keepExpandedButtons: PeerInfoScreenKeepExpandedButtons) { self.controller = controller self.context = context self.peerId = peerId @@ -986,7 +1043,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.scrollNode = ASScrollNode() self.scrollNode.view.delaysContentTouches = false - self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded) + self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded, keepExpandedButtons: keepExpandedButtons) self.paneContainerNode = PeerInfoPaneContainerNode(context: context, peerId: peerId) super.init() @@ -1057,6 +1114,15 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, openPeerInfo: { [weak self] peer in self?.openPeerInfo(peer: peer) + }, + performMemberAction: { [weak self] member, action in + self?.performMemberAction(member: member, action: action) + }, + openPeerInfoContextMenu: { [weak self] subject, sourceNode in + self?.openPeerInfoContextMenu(subject: subject, sourceNode: sourceNode) + }, + performBioLinkAction: { [weak self] action, item in + self?.performBioLinkAction(action: action, item: item) } ) @@ -1429,6 +1495,36 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } + self.paneContainerNode.requestExpandTabs = { [weak self] in + guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else { + return false + } + let contentOffset = strongSelf.scrollNode.view.contentOffset + let paneAreaExpansionFinalPoint: CGFloat = strongSelf.paneContainerNode.frame.minY - navigationHeight + if contentOffset.y < paneAreaExpansionFinalPoint - CGFloat.ulpOfOne { + strongSelf.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: paneAreaExpansionFinalPoint), animated: true) + return true + } else { + return false + } + } + + self.paneContainerNode.requestPerformPeerMemberAction = { [weak self] member, action in + guard let strongSelf = self else { + return + } + switch action { + case .open: + strongSelf.openPeerInfo(peer: member.peer) + case .promote: + strongSelf.performMemberAction(member: member, action: .promote) + case .restrict: + strongSelf.performMemberAction(member: member, action: .restrict) + case .remove: + strongSelf.performMemberAction(member: member, action: .remove) + } + } + self.headerNode.performButtonAction = { [weak self] key in self?.performButtonAction(key: key) } @@ -1612,6 +1708,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.hiddenAvatarRepresentationDisposable.dispose() self.toggleShouldChannelMessagesSignaturesDisposable.dispose() self.updateAvatarDisposable.dispose() + self.selectAddMemberDisposable.dispose() + self.addMemberDisposable.dispose() } override func didLoad() { @@ -1621,7 +1719,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private func updateData(_ data: PeerInfoScreenData) { self.data = data if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: self.didSetReady ? .animated(duration: 0.3, curve: .spring) : .immediate) } } @@ -1725,7 +1823,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (strongSelf.controller?.navigationController as? NavigationController)?.pushViewController(infoController) } } @@ -1911,7 +2009,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.view.endEditing(true) controller.present(actionSheet, in: .window(.root)) case .addMember: - break + self.openAddMember() } } @@ -2482,11 +2580,91 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } private func openPeerInfo(peer: Peer) { - if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (self.controller?.navigationController as? NavigationController)?.pushViewController(infoController) } } + private func performMemberAction(member: PeerInfoMember, action: PeerInfoMemberAction) { + guard let data = self.data, let peer = data.peer else { + return + } + switch action { + case .promote: + if case let .channelMember(channelMember) = member { + self.controller?.push(channelAdminController(context: self.context, peerId: peer.id, adminId: member.id, initialParticipant: channelMember.participant, updated: { _ in + }, upgradedToSupergroup: { _, f in f() }, transferedOwnership: { _ in })) + } + case .restrict: + if case let .channelMember(channelMember) = member { + self.controller?.push(channelBannedMemberController(context: self.context, peerId: peer.id, memberId: member.id, initialParticipant: channelMember.participant, updated: { _ in + }, upgradedToSupergroup: { _, f in f() })) + } + case .remove: + data.members?.membersContext.removeMember(memberId: member.id) + } + } + + private func openPeerInfoContextMenu(subject: PeerInfoContextSubject, sourceNode: ASDisplayNode) { + guard let data = self.data, let peer = data.peer, let controller = self.controller else { + return + } + switch subject { + case .bio: + var text: String? + if let cachedData = data.cachedData as? CachedUserData { + text = cachedData.about + } else if let cachedData = data.cachedData as? CachedGroupData { + text = cachedData.about + } else if let cachedData = data.cachedData as? CachedChannelData { + text = cachedData.about + } + if let text = text, !text.isEmpty { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = text + })]) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + return (sourceNode, sourceNode.bounds.insetBy(dx: 0.0, dy: -2.0), controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) + } + case let .phone(phone): + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = phone + })]) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + return (sourceNode, sourceNode.bounds.insetBy(dx: 0.0, dy: -2.0), controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) + case .link: + if let addressName = peer.addressName { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { + UIPasteboard.general.string = addressName + })]) + controller.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self, weak sourceNode] in + if let controller = self?.controller, let sourceNode = sourceNode { + return (sourceNode, sourceNode.bounds.insetBy(dx: 0.0, dy: -2.0), controller.displayNode, controller.view.bounds) + } else { + return nil + } + })) + } + } + } + + private func performBioLinkAction(action: TextLinkItemActionType, item: TextLinkItem) { + guard let data = self.data, let peer = data.peer, let controller = self.controller else { + return + } + self.context.sharedContext.handleTextLinkAction(context: self.context, peerId: peer.id, navigateDisposable: self.resolveUrlDisposable, controller: controller, action: action, itemLink: item) + } + private func openDeletePeer() { let peerId = self.peerId let _ = (self.context.account.postbox.transaction { transaction -> Peer? in @@ -2712,7 +2890,269 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }) } - func deleteMessages(messageIds: Set?) { + private func openAddMember() { + guard let data = self.data, let groupPeer = data.peer else { + return + } + + let members: Promise<[PeerId]> = Promise() + if groupPeer.id.namespace == Namespaces.Peer.CloudChannel { + /*var membersDisposable: Disposable? + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { listState in + members.set(.single(listState.list.map {$0.peer.id})) + membersDisposable?.dispose() + }) + membersDisposable = disposable*/ + members.set(.single([])) + } else { + members.set(.single([])) + } + + let _ = (members.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] recentIds in + guard let strongSelf = self else { + return + } + var createInviteLinkImpl: (() -> Void)? + var confirmationImpl: ((PeerId) -> Signal)? + var options: [ContactListAdditionalOption] = [] + let presentationData = strongSelf.presentationData + + var canCreateInviteLink = false + if let group = groupPeer as? TelegramGroup { + switch group.role { + case .creator, .admin: + canCreateInviteLink = true + default: + break + } + } else if let channel = groupPeer as? TelegramChannel { + if channel.hasPermission(.inviteMembers) { + if channel.flags.contains(.isCreator) || (channel.adminRights != nil && channel.username == nil) { + canCreateInviteLink = true + } + } + } + + if canCreateInviteLink { + options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: { + createInviteLinkImpl?() + })) + } + + let contactsController: ViewController + if groupPeer.id.namespace == Namespaces.Peer.CloudGroup { + contactsController = strongSelf.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: strongSelf.context, autoDismiss: false, title: { $0.GroupInfo_AddParticipantTitle }, options: options, confirmation: { peer in + if let confirmationImpl = confirmationImpl, case let .peer(peer, _, _) = peer { + return confirmationImpl(peer.id) + } else { + return .single(false) + } + })) + contactsController.navigationPresentation = .modal + } else { + contactsController = strongSelf.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: strongSelf.context, mode: .peerSelection(searchChatList: false, searchGroups: false), options: options, filters: [.excludeSelf, .disable(recentIds)])) + contactsController.navigationPresentation = .modal + } + + let context = strongSelf.context + confirmationImpl = { [weak contactsController] peerId in + return context.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue + |> mapToSignal { peer in + let result = ValuePromise() + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let contactsController = contactsController { + let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).0, actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: { + result.set(false) + }), + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: { + result.set(true) + }) + ]) + contactsController.present(alertController, in: .window(.root)) + } + + return result.get() + } + } + + let addMember: (ContactListPeer) -> Signal = { memberPeer -> Signal in + if case let .peer(selectedPeer, _, _) = memberPeer { + let memberId = selectedPeer.id + if groupPeer.id.namespace == Namespaces.Peer.CloudChannel { + return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: groupPeer.id, memberId: memberId) + |> map { _ -> Void in + return Void() + } + |> `catch` { _ -> Signal in + return .complete() + } + } else { + return addGroupMember(account: context.account, peerId: groupPeer.id, memberId: memberId) + |> deliverOnMainQueue + |> `catch` { error -> Signal in + switch error { + case .generic: + return .complete() + case .privacy: + let _ = (context.account.postbox.loadedPeerWithId(memberId) + |> deliverOnMainQueue).start(next: { peer in + self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + return .complete() + case .tooManyChannels: + let _ = (context.account.postbox.loadedPeerWithId(memberId) + |> deliverOnMainQueue).start(next: { peer in + self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + return .complete() + case .groupFull: + let signal = convertGroupToSupergroup(account: context.account, peerId: groupPeer.id) + |> map(Optional.init) + |> `catch` { error -> Signal in + switch error { + case .tooManyChannels: + Queue.mainQueue().async { + self?.controller?.push(oldChannelsController(context: context, intent: .upgrade)) + } + default: + break + } + return .single(nil) + } + |> mapToSignal { upgradedPeerId -> Signal in + guard let upgradedPeerId = upgradedPeerId else { + return .single(nil) + } + return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: upgradedPeerId, memberId: memberId) + |> `catch` { _ -> Signal in + return .complete() + } + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(upgradedPeerId)) + } + |> deliverOnMainQueue + |> mapToSignal { _ -> Signal in + return .complete() + } + return signal + } + } + } + } else { + return .complete() + } + } + + let addMembers: ([ContactListPeerId]) -> Signal = { members -> Signal in + let memberIds = members.compactMap { contact -> PeerId? in + switch contact { + case let .peer(peerId): + return peerId + default: + return nil + } + } + return context.account.postbox.multiplePeersView(memberIds) + |> take(1) + |> deliverOnMainQueue + |> mapError { _ in return .generic} + |> mapToSignal { view -> Signal in + if memberIds.count == 1 { + return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: groupPeer.id, memberId: memberIds[0]) + |> map { _ -> Void in + return Void() + } + } else { + return context.peerChannelMemberCategoriesContextsManager.addMembers(account: context.account, peerId: groupPeer.id, memberIds: memberIds) |> map { _ in + } + } + } + } + + createInviteLinkImpl = { [weak contactsController] in + guard let strongSelf = self else { + return + } + let mode: ChannelVisibilityControllerMode + if groupPeer.addressName != nil { + mode = .generic + } else { + mode = .privateLink + } + let visibilityController = channelVisibilityController(context: strongSelf.context, peerId: groupPeer.id, mode: mode, upgradedToSupergroup: { _, f in f() }) + visibilityController.navigationPresentation = .modal + + if let navigationController = strongSelf.controller?.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + if let contactsController = contactsController { + controllers.removeAll(where: { $0 === contactsController }) + } + controllers.append(visibilityController) + navigationController.setViewControllers(controllers, animated: true) + } + } + + strongSelf.controller?.push(contactsController) + let selectAddMemberDisposable = strongSelf.selectAddMemberDisposable + let addMemberDisposable = strongSelf.addMemberDisposable + if let contactsController = contactsController as? ContactSelectionController { + selectAddMemberDisposable.set((contactsController.result + |> deliverOnMainQueue).start(next: { [weak contactsController] memberPeer in + guard let memberPeer = memberPeer else { + return + } + + contactsController?.displayProgress = true + addMemberDisposable.set((addMember(memberPeer) + |> deliverOnMainQueue).start(completed: { + contactsController?.dismiss() + })) + })) + contactsController.dismissed = { + selectAddMemberDisposable.set(nil) + addMemberDisposable.set(nil) + } + } + if let contactsController = contactsController as? ContactMultiselectionController { + selectAddMemberDisposable.set((contactsController.result + |> deliverOnMainQueue).start(next: { [weak contactsController] peers in + contactsController?.displayProgress = true + addMemberDisposable.set((addMembers(peers) + |> deliverOnMainQueue).start(error: { error in + if peers.count == 1, case .restricted = error { + switch peers[0] { + case let .peer(peerId): + let _ = (context.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }) + default: + break + } + } else if case .tooMuchJoined = error { + self?.controller?.present(textAlertController(context: context, title: nil, text: presentationData.strings.Invite_ChannelsTooMuch, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + + contactsController?.dismiss() + },completed: { + contactsController?.dismiss() + })) + })) + contactsController.dismissed = { + selectAddMemberDisposable.set(nil) + addMemberDisposable.set(nil) + } + } + }) + } + + private func deleteMessages(messageIds: Set?) { if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty { self.activeActionDisposable.set((self.context.sharedContext.chatAvailableMessageActions(postbox: self.context.account.postbox, accountPeerId: self.context.account.peerId, messageIds: messageIds) |> deliverOnMainQueue).start(next: { [weak self] actions in @@ -3351,10 +3791,16 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } +public enum PeerInfoScreenKeepExpandedButtons { + case message + case mute +} + public final class PeerInfoScreen: ViewController { private let context: AccountContext private let peerId: PeerId private let avatarInitiallyExpanded: Bool + private let keepExpandedButtons: PeerInfoScreenKeepExpandedButtons private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -3368,10 +3814,13 @@ public final class PeerInfoScreen: ViewController { return self._ready } - public init(context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool = false) { + private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + public init(context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool = false, keepExpandedButtons: PeerInfoScreenKeepExpandedButtons = .message) { self.context = context self.peerId = peerId self.avatarInitiallyExpanded = avatarInitiallyExpanded + self.keepExpandedButtons = keepExpandedButtons self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -3439,7 +3888,7 @@ public final class PeerInfoScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded) + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, keepExpandedButtons: self.keepExpandedButtons) self._ready.set(self.controllerNode.ready.get()) @@ -3448,11 +3897,17 @@ public final class PeerInfoScreen: ViewController { override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + + if let (layout, navigationHeight) = self.validLayout { + self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) + self.validLayout = (layout, navigationHeight) + self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) } } diff --git a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift index 7374fc9577..d2ff44fa00 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift @@ -759,7 +759,7 @@ public class PeerMediaCollectionController: TelegramBaseController { |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } } diff --git a/submodules/TelegramUI/TelegramUI/PollResultsController.swift b/submodules/TelegramUI/TelegramUI/PollResultsController.swift index a1a24b756a..4a7e627a71 100644 --- a/submodules/TelegramUI/TelegramUI/PollResultsController.swift +++ b/submodules/TelegramUI/TelegramUI/PollResultsController.swift @@ -303,7 +303,7 @@ public func pollResultsController(context: AccountContext, messageId: MessageId, }) }, openPeer: { peer in if let peer = peer.peers[peer.peerId] { - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { pushControllerImpl?(controller) } } diff --git a/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift b/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift index 8a475cb359..f32768d66b 100644 --- a/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift +++ b/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift @@ -1004,8 +1004,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { handleTextLinkActionImpl(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink) } - public func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool) -> ViewController? { - let controller = peerInfoControllerImpl(context: context, peer: peer, mode: mode, avatarInitiallyExpanded: avatarInitiallyExpanded) + public func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool) -> ViewController? { + let controller = peerInfoControllerImpl(context: context, peer: peer, mode: mode, avatarInitiallyExpanded: avatarInitiallyExpanded, keepExpandedButtons: fromChat ? .mute : .message) controller?.navigationPresentation = .modalInLargeLayout return controller } @@ -1245,13 +1245,13 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let defaultChatControllerInteraction = ChatControllerInteraction.default -private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool) -> ViewController? { +private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, keepExpandedButtons: PeerInfoScreenKeepExpandedButtons = .message) -> ViewController? { if let _ = peer as? TelegramGroup { - return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded) + return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, keepExpandedButtons: keepExpandedButtons) return groupInfoController(context: context, peerId: peer.id) } else if let channel = peer as? TelegramChannel { - return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded) + return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, keepExpandedButtons: keepExpandedButtons) if case .group = channel.info { return groupInfoController(context: context, peerId: peer.id) @@ -1259,7 +1259,7 @@ private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: P return channelInfoController(context: context, peerId: peer.id) } } else if peer is TelegramUser { - return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded) + return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, keepExpandedButtons: keepExpandedButtons) } else if peer is TelegramSecretChat { return userInfoController(context: context, peerId: peer.id, mode: mode) } diff --git a/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift b/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift index d8d1fd0fb2..6579d898c6 100644 --- a/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift +++ b/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift @@ -32,7 +32,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate peerSignal = context.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init) navigateDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { peer in if let controller = controller, let peer = peer { - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) { (controller.navigationController as? NavigationController)?.pushViewController(infoController) } } diff --git a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift index a1e70f6726..b5e386a3a8 100644 --- a/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift +++ b/submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift @@ -27,9 +27,34 @@ private final class PeerChannelMembersOnlineContext { } } +private final class ProfileDataPreloadContext { + let subscribers = Bag<() -> Void>() + + let disposable: Disposable + var emptyTimer: SwiftSignalKit.Timer? + + init(disposable: Disposable) { + self.disposable = disposable + } +} + +private final class ProfileDataPhotoPreloadContext { + let subscribers = Bag<(Any?) -> Void>() + + let disposable: Disposable + var value: Any? + var emptyTimer: SwiftSignalKit.Timer? + + init(disposable: Disposable) { + self.disposable = disposable + } +} + private final class PeerChannelMemberCategoriesContextsManagerImpl { fileprivate var contexts: [PeerId: PeerChannelMemberCategoriesContext] = [:] fileprivate var onlineContexts: [PeerId: PeerChannelMembersOnlineContext] = [:] + fileprivate var profileDataPreloadContexts: [PeerId: ProfileDataPreloadContext] = [:] + fileprivate var profileDataPhotoPreloadContexts: [PeerId: ProfileDataPhotoPreloadContext] = [:] func getContext(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { if let current = self.contexts[peerId] { @@ -121,6 +146,121 @@ private final class PeerChannelMemberCategoriesContextsManagerImpl { context.loadMore(control) } } + + func profileData(postbox: Postbox, network: Network, peerId: PeerId, customData: Signal?) -> Disposable { + let context: ProfileDataPreloadContext + if let current = self.profileDataPreloadContexts[peerId] { + context = current + } else { + let disposable = DisposableSet() + context = ProfileDataPreloadContext(disposable: disposable) + self.profileDataPreloadContexts[peerId] = context + + if let customData = customData { + disposable.add(customData.start()) + } + + /*disposable.set(signal.start(next: { [weak context] value in + guard let context = context else { + return + } + context.value = value + for f in context.subscribers.copyItems() { + f(value) + } + }))*/ + } + + if let emptyTimer = context.emptyTimer { + emptyTimer.invalidate() + context.emptyTimer = nil + } + + let index = context.subscribers.add({ + }) + //updated(context.value ?? 0) + + return ActionDisposable { [weak self, weak context] in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if let current = strongSelf.profileDataPreloadContexts[peerId], let context = context, current === context { + current.subscribers.remove(index) + if current.subscribers.isEmpty { + if current.emptyTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 60.0, repeat: false, completion: { [weak context] in + if let current = strongSelf.profileDataPreloadContexts[peerId], let context = context, current === context { + if current.subscribers.isEmpty { + strongSelf.profileDataPreloadContexts.removeValue(forKey: peerId) + current.disposable.dispose() + } + } + }, queue: Queue.mainQueue()) + current.emptyTimer = timer + timer.start() + } + } + } + } + } + } + + func profilePhotos(postbox: Postbox, network: Network, peerId: PeerId, fetch: Signal, updated: @escaping (Any?) -> Void) -> Disposable { + let context: ProfileDataPhotoPreloadContext + if let current = self.profileDataPhotoPreloadContexts[peerId] { + context = current + } else { + let disposable = MetaDisposable() + context = ProfileDataPhotoPreloadContext(disposable: disposable) + self.profileDataPhotoPreloadContexts[peerId] = context + + disposable.set(fetch.start(next: { [weak context] value in + guard let context = context else { + return + } + context.value = value + for f in context.subscribers.copyItems() { + f(value) + } + })) + } + + if let emptyTimer = context.emptyTimer { + emptyTimer.invalidate() + context.emptyTimer = nil + } + + let index = context.subscribers.add({ next in + updated(next) + }) + updated(context.value) + + return ActionDisposable { [weak self, weak context] in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if let current = strongSelf.profileDataPhotoPreloadContexts[peerId], let context = context, current === context { + current.subscribers.remove(index) + if current.subscribers.isEmpty { + if current.emptyTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 60.0, repeat: false, completion: { [weak context] in + if let current = strongSelf.profileDataPhotoPreloadContexts[peerId], let context = context, current === context { + if current.subscribers.isEmpty { + strongSelf.profileDataPhotoPreloadContexts.removeValue(forKey: peerId) + current.disposable.dispose() + } + } + }, queue: Queue.mainQueue()) + current.emptyTimer = timer + timer.start() + } + } + } + } + } + } } public final class PeerChannelMemberCategoriesContextsManager { @@ -394,4 +534,35 @@ public final class PeerChannelMemberCategoriesContextsManager { } |> runOn(Queue.mainQueue()) } + + public func profileData(postbox: Postbox, network: Network, peerId: PeerId, customData: Signal?) -> Signal { + return Signal { [weak self] subscriber in + guard let strongSelf = self else { + subscriber.putCompletion() + return EmptyDisposable + } + let disposable = strongSelf.impl.syncWith({ impl -> Disposable in + return impl.profileData(postbox: postbox, network: network, peerId: peerId, customData: customData) + }) + return disposable ?? EmptyDisposable + } + |> runOn(Queue.mainQueue()) + } + + public func profilePhotos(postbox: Postbox, network: Network, peerId: PeerId, fetch: Signal) -> Signal { + return Signal { [weak self] subscriber in + guard let strongSelf = self else { + subscriber.putNext(0) + subscriber.putCompletion() + return EmptyDisposable + } + let disposable = strongSelf.impl.syncWith({ impl -> Disposable in + return impl.profilePhotos(postbox: postbox, network: network, peerId: peerId, fetch: fetch, updated: { value in + subscriber.putNext(value) + }) + }) + return disposable ?? EmptyDisposable + } + |> runOn(Queue.mainQueue()) + } }