Bug fixes and performance improvements

This commit is contained in:
Ali 2020-02-11 15:42:35 +01:00
parent 76893115f6
commit 580e1cebc5
35 changed files with 1393 additions and 135 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<PeerInfoScreenData, NoError> {
private func peerInfoScreenInputData(context: AccountContext, peerId: PeerId) -> Signal<PeerInfoScreenInputData, NoError> {
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<Any, NoError> {
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<Never, NoError> {
return peerInfoScreenInputData(context: context, peerId: peerId)
|> mapToSignal { inputData -> Signal<Never, NoError> 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<PeerInfoScreenData, NoError> {
return peerInfoScreenInputData(context: context, peerId: peerId)
|> mapToSignal { inputData -> Signal<PeerInfoScreenData, NoError> 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<PeerInfoMembersData?, NoError> = 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<ValueBoxKey>([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 {

View File

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

View File

@ -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<PeerInfoMembersState>()
var state: Signal<PeerInfoMembersState, NoError> {
@ -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<Never, NoError>
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
}

View File

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

View File

@ -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<TGMediaAvatarMenuMixin?>(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<MessageId>?) {
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<Bool, NoError>)?
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<Bool>()
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<Void, NoError> = { memberPeer -> Signal<Void, NoError> 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<Void, NoError> in
return .complete()
}
} else {
return addGroupMember(account: context.account, peerId: groupPeer.id, memberId: memberId)
|> deliverOnMainQueue
|> `catch` { error -> Signal<Void, NoError> 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<PeerId?, NoError> in
switch error {
case .tooManyChannels:
Queue.mainQueue().async {
self?.controller?.push(oldChannelsController(context: context, intent: .upgrade))
}
default:
break
}
return .single(nil)
}
|> mapToSignal { upgradedPeerId -> Signal<PeerId?, NoError> in
guard let upgradedPeerId = upgradedPeerId else {
return .single(nil)
}
return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: upgradedPeerId, memberId: memberId)
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<PeerId?, NoError> in
return .complete()
}
|> then(.single(upgradedPeerId))
}
|> deliverOnMainQueue
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
return signal
}
}
}
} else {
return .complete()
}
}
let addMembers: ([ContactListPeerId]) -> Signal<Void, AddChannelMemberError> = { members -> Signal<Void, AddChannelMemberError> 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<Void, AddChannelMemberError> 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<MessageId>?) {
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Never, NoError>?) -> 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<Any, NoError>, 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<Never, NoError>?) -> Signal<Never, NoError> {
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<Any, NoError>) -> Signal<Any?, NoError> {
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())
}
}