import Foundation import UIKit import Display import AsyncDisplayKit import TelegramCore import SwiftSignalKit import AccountContext import TelegramPresentationData import ComponentFlow import ViewControllerComponent import SheetComponent import MultilineTextComponent import GlassBarButtonComponent import ButtonComponent import PresentationDataUtils import BundleIconComponent import ListSectionComponent import ListActionItemComponent import ListTextFieldItemComponent import ListItemComponentAdaptor import GlassBackgroundComponent import ItemListAvatarAndNameInfoItem import ItemListUI import PeerInfoUI import UndoUI import RankChatPreviewItem private let rankFieldTag = GenericComponentViewTag() private let rankMaxLength: Int32 = 16 private final class ChatParticipantRightsContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let subject: ChatParticipantRightsScreen.Subject let cancel: (Bool) -> Void init( context: AccountContext, subject: ChatParticipantRightsScreen.Subject, cancel: @escaping (Bool) -> Void ) { self.context = context self.subject = subject self.cancel = cancel } static func ==(lhs: ChatParticipantRightsContent, rhs: ChatParticipantRightsContent) -> Bool { if lhs.context !== rhs.context { return false } return true } final class State: ComponentState { private let context: AccountContext private let subject: ChatParticipantRightsScreen.Subject var disposable: Disposable? var peer: EnginePeer? var presence: EnginePeer.Presence? var rank: String? var initialRank: String? weak var controller: ViewController? init(context: AccountContext, subject: ChatParticipantRightsScreen.Subject) { self.context = context self.subject = subject super.init() let participantId: EnginePeer.Id switch subject { case let .rank(_, participantIdValue, rankValue, _): participantId = participantIdValue self.rank = rankValue self.initialRank = rankValue } self.disposable = (context.engine.data.subscribe( TelegramEngine.EngineData.Item.Peer.Peer(id: participantId), TelegramEngine.EngineData.Item.Peer.Presence(id: participantId) ) |> deliverOnMainQueue).start(next: { [weak self] peer, presence in guard let self else { return } self.peer = peer self.presence = presence self.updated() }) } deinit { self.disposable?.dispose() } func animateError() { guard let controller = self.controller as? ChatParticipantRightsScreen else { return } controller.animateError() } func complete() { guard let controller = self.controller as? ChatParticipantRightsScreen else { return } controller.complete(rank: self.rank) } } func makeState() -> State { return State(context: self.context, subject: self.subject) } static var body: Body { let closeButton = Child(GlassBarButtonComponent.self) let title = Child(MultilineTextComponent.self) let rankSection = Child(ListSectionComponent.self) let doneButton = Child(ButtonComponent.self) return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component let theme = environment.theme.withModalBlocksBackground() let strings = environment.strings let state = context.state if state.controller == nil { state.controller = environment.controller() } let sideInset: CGFloat = 16.0 + environment.safeInsets.left let closeButton = closeButton.update( component: GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: nil, isDark: theme.overallDarkAppearance, state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", tintColor: theme.chat.inputPanel.panelControlColor ) )), action: { _ in component.cancel(true) } ), availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) var contentHeight: CGFloat = 38.0 let titleString: String switch component.subject { case .rank: titleString = strings.EditRank_Title } let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight)) ) contentHeight += 44.0 let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let listItemParams = ListViewItemLayoutParams(width: context.availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) let peer: EnginePeer if let current = state.peer { peer = current } else { peer = EnginePeer.user(TelegramUser(id: component.subject.participantId, accessHash: nil, firstName: " ", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)) } var rankPreviewPlaceholder = "" var rankFooterString = "" var rankRole: ChatRankInfoScreenRole = .member switch component.subject { case let .rank(_, _, _, role): if peer.id == component.context.account.peerId { rankFooterString = strings.EditRank_InfoYou } else { rankFooterString = strings.EditRank_Info(peer.compactDisplayTitle).string } switch role { case .creator: rankPreviewPlaceholder = strings.Conversation_Owner case .admin: rankPreviewPlaceholder = strings.Conversation_Admin case .member: rankPreviewPlaceholder = "0️⃣" } rankRole = role } let rankValue = state.rank ?? "" let messageItem = RankChatPreviewItem.MessageItem( peer: peer, text: "Reinhardt, we need to find you some new tunes.", entities: nil, media: [], rank: rankValue.isEmpty ? rankPreviewPlaceholder : rankValue, rankRole: rankRole ) var rankSectionItems: [AnyComponentWithIdentity] = [] rankSectionItems.append( AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( itemGenerator: RankChatPreviewItem( context: component.context, theme: environment.theme, componentTheme: theme, strings: strings, sectionId: 0, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, wallpaper: presentationData.chatWallpaper, dateTimeFormat: environment.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, messageItems: [messageItem] ), params: listItemParams ))) ) rankSectionItems.append( AnyComponentWithIdentity(id: 1, component: AnyComponent(ListTextFieldItemComponent( style: .glass, theme: theme, initialText: state.initialRank ?? "", resetText: nil, placeholder: strings.EditRank_Placeholder, characterLimit: Int(rankMaxLength), autocapitalizationType: .sentences, autocorrectionType: .default, returnKeyType: .done, contentInsets: .zero, updated: { [weak state] value in guard let state else { return } state.rank = value state.updated(transition: .easeInOut(duration: 0.2)) }, shouldUpdateText: { [weak state] text in if text.containsEmoji { state?.animateError() return false } return true }, onReturn: { [weak state] in guard let state else { return } state.complete() }, tag: rankFieldTag ))) ) let rankSection = rankSection.update( component: ListSectionComponent( theme: theme, style: .glass, header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: rankFooterString, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor )), maximumNumberOfLines: 0 )), items: rankSectionItems ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: context.transition ) context.add(rankSection .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + rankSection.size.height / 2.0)) ) contentHeight += rankSection.size.height contentHeight += 24.0 let buttonTitle: String switch component.subject { case let .rank(_, _, initialRank, _): if (initialRank ?? "").isEmpty && (state.rank ?? "").isEmpty { buttonTitle = strings.EditRank_AddLater } else if (initialRank ?? "").isEmpty && !(state.rank ?? "").isEmpty { buttonTitle = strings.EditRank_AddTag } else if !(initialRank ?? "").isEmpty && (state.rank ?? "").isEmpty { buttonTitle = strings.EditRank_RemoveTag } else { buttonTitle = strings.EditRank_EditTag } } let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let doneButton = doneButton.update( component: ButtonComponent( background: ButtonComponent.Background( style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: AnyComponentWithIdentity( id: AnyHashable(buttonTitle), component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) ), action: { [weak state] in guard let state else { return } state.complete() } ), availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: context.transition ) context.add(doneButton .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + doneButton.size.height / 2.0)) ) contentHeight += doneButton.size.height if environment.inputHeight > 0.0 { contentHeight += 15.0 contentHeight += max(environment.inputHeight, environment.safeInsets.bottom) } else { contentHeight += buttonInsets.bottom } return CGSize(width: context.availableSize.width, height: contentHeight) } } } private final class ChatParticipantRightsComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let subject: ChatParticipantRightsScreen.Subject init( context: AccountContext, subject: ChatParticipantRightsScreen.Subject ) { self.context = context self.subject = subject } static func ==(lhs: ChatParticipantRightsComponent, rhs: ChatParticipantRightsComponent) -> Bool { if lhs.context !== rhs.context { return false } return true } static var body: Body { let sheet = Child(SheetComponent.self) let animateOut = StoredActionSlot(Action.self) return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller let sheet = sheet.update( component: SheetComponent( content: AnyComponent(ChatParticipantRightsContent( context: context.component.context, subject: context.component.subject, cancel: { animate in if animate { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } else if let controller = controller() { controller.dismiss(animated: false, completion: nil) } } )), style: .glass, backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), followContentSizeChanges: false, clipsContent: true, isScrollEnabled: false, animateOut: animateOut ), environment: { environment SheetComponentEnvironment( metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, hasInputHeight: !environment.inputHeight.isZero, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in if animated { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } else { if let controller = controller() { controller.dismiss(completion: nil) } } } ) }, availableSize: context.availableSize, transition: context.transition ) context.add(sheet .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) return context.availableSize } } } public class ChatParticipantRightsScreen: ViewControllerComponentContainer { public enum Subject { case rank(peerId: EnginePeer.Id, participantId: EnginePeer.Id, rank: String?, role: ChatRankInfoScreenRole) var peerId: EnginePeer.Id { switch self { case let .rank(peerId, _, _, _): return peerId } } var participantId: EnginePeer.Id { switch self { case let .rank(_, participantId, _, _): return participantId } } } private let context: AccountContext private let subject: Subject public init( context: AccountContext, subject: Subject ) { self.context = context self.subject = subject super.init( context: context, component: ChatParticipantRightsComponent( context: context, subject: subject ), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: .default ) self.navigationPresentation = .flatModal self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func viewDidLoad() { super.viewDidLoad() self.view.disablesInteractiveModalDismiss = true } fileprivate func complete(rank: String?) { var rank = rank if rank?.isEmpty == true { rank = nil } if let rank, rank.count > rankMaxLength { if let view = self.node.hostView.findTaggedView(tag: rankFieldTag) as? ListTextFieldItemComponent.View { view.animateError() HapticFeedback().error() } return } let _ = self.context.peerChannelMemberCategoriesContextsManager.updateMemberRank(engine: self.context.engine, peerId: self.subject.peerId, memberId: self.subject.participantId, rank: rank).start() let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } if let navigationController = self.navigationController as? NavigationController { Queue.mainQueue().after(0.5) { var hadRank = false if case let .rank(_, _, rank, _) = self.subject, !(rank ?? "").isEmpty { hadRank = true } var title: String? var text: String? if let rank { title = hadRank ? presentationData.strings.Chat_TagUpdated_Edited : presentationData.strings.Chat_TagUpdated_Added text = rank } else if hadRank { text = presentationData.strings.Chat_TagUpdated_Removed } if let text { let toastController = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: title, text: text, cancel: nil, destructive: false), appearance: .init(isNarrow: true), action: { _ in return true}) (navigationController.topViewController as? ViewController)?.present(toastController, in: .current) } } } self.dismissAnimated() } fileprivate func animateError() { if let view = self.node.hostView.findTaggedView(tag: rankFieldTag) as? ListTextFieldItemComponent.View { view.animateError() } } public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if let view = self.node.hostView.findTaggedView(tag: rankFieldTag) as? ListTextFieldItemComponent.View { Queue.mainQueue().after(0.01) { view.activateInput() view.selectAll() } } } public func dismissAnimated() { if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { view.dismissAnimated() } } }