diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b39f1ace52..83e472d9c4 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8097,6 +8097,14 @@ Sorry for the inconvenience."; "Username.LinksOrder" = "USERNAMES ORDER"; "Username.LinksOrderInfo" = "Drag and drop links to change the order in which they will be displayed on your info page."; +"Username.ActivateAlertTitle" = "Activate Username"; +"Username.ActivateAlertText" = "Do you want to show this link on your info page?"; +"Username.ActivateAlertShow" = "Show"; + +"Username.DeactivateAlertTitle" = "Deativate Username"; +"Username.DeactivateAlertText" = "Do you want to hode this link from your info page?"; +"Username.DeactivateAlertHide" = "Hide"; + "Profile.AdditionalUsernames" = "also %@"; "EmojiSearch.SearchReactionsPlaceholder" = "Search Reactions"; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5d017b3aee..883c71a2c8 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1427,13 +1427,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.chatListDisplayNode.emptyListAction = { [weak self] in - guard let strongSelf = self else { + guard let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController else { return } if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter { strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in })) } else { - strongSelf.composePressed() + if case let .forum(peerId) = strongSelf.location { + let context = strongSelf.context + let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create) + controller.navigationPresentation = .modal + controller.completion = { title, fileId in + let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconFileId: fileId) + |> deliverOnMainQueue).start(next: { topicId in + let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, navigationController: navigationController).start() + +// let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "First Message", attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(topicId)), localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + }) + } + strongSelf.push(controller) + } else { + strongSelf.composePressed() + } } } @@ -2394,10 +2409,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }, action: { action in action.dismissWithResult(.default) - let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: "Topic#\(Int.random(in: 0 ..< 100000))", iconFileId: nil) - |> deliverOnMainQueue).start(next: { topicId in - let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "First Message", attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(topicId)), localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() - }) + let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create) + controller.navigationPresentation = .modal + controller.completion = { title, fileId in + let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconFileId: fileId) + |> deliverOnMainQueue).start(next: { topicId in + let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: "First Message", attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(topicId)), localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + }) + } + sourceController.push(controller) }))) let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 2e225b345c..f70f166ce4 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -286,6 +286,7 @@ private final class ChatListContainerItemNode: ASDisplayNode { private var presentationData: PresentationData private let becameEmpty: (ChatListFilter?) -> Void private let emptyAction: (ChatListFilter?) -> Void + private let secondaryEmptyAction: () -> Void private var floatingHeaderOffset: CGFloat? @@ -296,13 +297,14 @@ private final class ChatListContainerItemNode: ASDisplayNode { private var validLayout: (CGSize, UIEdgeInsets, CGFloat)? - init(context: AccountContext, location: ChatListControllerLocation, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void) { + init(context: AccountContext, location: ChatListControllerLocation, filter: ChatListFilter?, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void) { self.context = context self.animationCache = animationCache self.animationRenderer = animationRenderer self.presentationData = presentationData self.becameEmpty = becameEmpty self.emptyAction = emptyAction + self.secondaryEmptyAction = secondaryEmptyAction self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: .chatList, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true) @@ -334,8 +336,21 @@ private final class ChatListContainerItemNode: ASDisplayNode { if let currentNode = strongSelf.emptyNode { currentNode.updateIsLoading(isLoading) } else { - let emptyNode = ChatListEmptyNode(context: context, isFilter: filter != nil, isLoading: isLoading, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, action: { + let subject: ChatListEmptyNode.Subject + if filter != nil { + subject = .filter + } else { + if case .forum = location { + subject = .forum + } else { + subject = .chats + } + } + + let emptyNode = ChatListEmptyNode(context: context, subject: subject, isLoading: isLoading, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, action: { self?.emptyAction(filter) + }, secondaryAction: { + self?.secondaryEmptyAction() }) strongSelf.emptyNode = emptyNode strongSelf.addSubnode(emptyNode) @@ -431,6 +446,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let controlsHistoryPreload: Bool private let filterBecameEmpty: (ChatListFilter?) -> Void private let filterEmptyAction: (ChatListFilter?) -> Void + private let secondaryEmptyAction: () -> Void fileprivate var onFilterSwitch: (() -> Void)? @@ -591,12 +607,13 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { var didBeginSelectingChats: (() -> Void)? var displayFilterLimit: (() -> Void)? - init(context: AccountContext, location: ChatListControllerLocation, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void) { + init(context: AccountContext, location: ChatListControllerLocation, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void) { self.context = context self.location = location self.previewing = previewing self.filterBecameEmpty = filterBecameEmpty self.filterEmptyAction = filterEmptyAction + self.secondaryEmptyAction = secondaryEmptyAction self.controlsHistoryPreload = controlsHistoryPreload self.presentationData = presentationData @@ -611,6 +628,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in self?.filterEmptyAction(filter) + }, secondaryEmptyAction: { [weak self] in + self?.secondaryEmptyAction() }) self.itemNodes[.all] = itemNode self.addSubnode(itemNode) @@ -887,6 +906,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in self?.filterEmptyAction(filter) + }, secondaryEmptyAction: { [weak self] in + self?.secondaryEmptyAction() }) let disposable = MetaDisposable() self.pendingItemNode = (id, itemNode, disposable) @@ -1015,6 +1036,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { self?.filterBecameEmpty(filter) }, emptyAction: { [weak self] filter in self?.filterEmptyAction(filter) + }, secondaryEmptyAction: { [weak self] in + self?.secondaryEmptyAction() }) self.itemNodes[id] = itemNode } @@ -1118,10 +1141,13 @@ final class ChatListControllerNode: ASDisplayNode { var filterBecameEmpty: ((ChatListFilter?) -> Void)? var filterEmptyAction: ((ChatListFilter?) -> Void)? + var secondaryEmptyAction: (() -> Void)? self.containerNode = ChatListContainerNode(context: context, location: location, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, presentationData: presentationData, animationCache: animationCache, animationRenderer: animationRenderer, filterBecameEmpty: { filter in filterBecameEmpty?(filter) }, filterEmptyAction: { filter in filterEmptyAction?(filter) + }, secondaryEmptyAction: { + secondaryEmptyAction?() }) self.inlineTabContainerNode = ChatListFilterTabInlineContainerNode() @@ -1156,6 +1182,15 @@ final class ChatListControllerNode: ASDisplayNode { strongSelf.emptyListAction?() } + secondaryEmptyAction = { [weak self] in + guard let strongSelf = self, case let .forum(peerId) = strongSelf.location, let controller = strongSelf.controller else { + return + } + + let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) + (controller.navigationController as? NavigationController)?.replaceController(controller, with: chatController, animated: false) + } + self.containerNode.onFilterSwitch = { [weak self] in if let strongSelf = self { strongSelf.controller?.dismissAllUndoControllers() diff --git a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift index 5a5ef59a83..2ca8fab6e6 100644 --- a/submodules/ChatListUI/Sources/ChatListEmptyNode.swift +++ b/submodules/ChatListUI/Sources/ChatListEmptyNode.swift @@ -11,24 +11,31 @@ import ActivityIndicator import AccountContext final class ChatListEmptyNode: ASDisplayNode { + enum Subject { + case chats + case filter + case forum + } private let action: () -> Void + private let secondaryAction: () -> Void - let isFilter: Bool + let subject: Subject private(set) var isLoading: Bool private let textNode: ImmediateTextNode private let descriptionNode: ImmediateTextNode private let animationNode: AnimatedStickerNode - private let buttonTextNode: ImmediateTextNode - private let buttonNode: HighlightTrackingButtonNode + private let buttonNode: SolidRoundedButtonNode + private let secondaryButtonNode: HighlightableButtonNode private let activityIndicator: ActivityIndicator private var animationSize: CGSize = CGSize() private var validLayout: CGSize? - init(context: AccountContext, isFilter: Bool, isLoading: Bool, theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { + init(context: AccountContext, subject: Subject, isLoading: Bool, theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void, secondaryAction: @escaping () -> Void) { self.action = action - self.isFilter = isFilter + self.secondaryAction = secondaryAction + self.subject = subject self.isLoading = isLoading self.animationNode = DefaultAnimatedStickerNodeImpl() @@ -47,10 +54,9 @@ final class ChatListEmptyNode: ASDisplayNode { self.descriptionNode.textAlignment = .center self.descriptionNode.lineSpacing = 0.1 - self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), cornerRadius: 11.0, gloss: true) - self.buttonTextNode = ImmediateTextNode() - self.buttonTextNode.displaysAsynchronously = false + self.secondaryButtonNode = HighlightableButtonNode() self.activityIndicator = ActivityIndicator(type: .custom(theme.list.itemAccentColor, 22.0, 1.0, false)) @@ -59,12 +65,12 @@ final class ChatListEmptyNode: ASDisplayNode { self.addSubnode(self.animationNode) self.addSubnode(self.textNode) self.addSubnode(self.descriptionNode) - self.addSubnode(self.buttonTextNode) self.addSubnode(self.buttonNode) + self.addSubnode(self.secondaryButtonNode) self.addSubnode(self.activityIndicator) let animationName: String - if isFilter { + if case .filter = subject { animationName = "ChatListFilterEmpty" } else { animationName = "ChatListEmpty" @@ -78,23 +84,15 @@ final class ChatListEmptyNode: ASDisplayNode { self.textNode.isHidden = self.isLoading self.descriptionNode.isHidden = self.isLoading self.buttonNode.isHidden = self.isLoading - self.buttonTextNode.isHidden = self.isLoading self.activityIndicator.isHidden = !self.isLoading self.buttonNode.hitTestSlop = UIEdgeInsets(top: -10.0, left: -10.0, bottom: -10.0, right: -10.0) - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - self.buttonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.buttonTextNode.layer.removeAnimation(forKey: "opacity") - strongSelf.buttonTextNode.alpha = 0.4 - } else { - strongSelf.buttonTextNode.alpha = 1.0 - strongSelf.buttonTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } + self.buttonNode.pressed = { [weak self] in + self?.buttonPressed() } + self.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside) + self.updateThemeAndStrings(theme: theme, strings: strings) self.animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationTapGesture(_:)))) @@ -104,6 +102,10 @@ final class ChatListEmptyNode: ASDisplayNode { self.action() } + @objc private func secondaryButtonPressed() { + self.secondaryAction() + } + @objc private func animationTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if !self.animationNode.isPlaying { @@ -117,18 +119,33 @@ final class ChatListEmptyNode: ASDisplayNode { } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - let string = NSMutableAttributedString(string: self.isFilter ? strings.ChatList_EmptyChatListFilterTitle : strings.ChatList_EmptyChatList, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor) - let descriptionString: NSAttributedString - if self.isFilter { - descriptionString = NSAttributedString(string: strings.ChatList_EmptyChatListFilterText, font: Font.regular(14.0), textColor: theme.list.itemSecondaryTextColor) - } else { - descriptionString = NSAttributedString() + let text: String + var descriptionText = "" + let buttonText: String + var secondaryButtonText = "" + switch self.subject { + case .chats: + text = strings.ChatList_EmptyChatList + buttonText = strings.ChatList_EmptyChatListNewMessage + case .filter: + text = strings.ChatList_EmptyChatListFilterTitle + descriptionText = strings.ChatList_EmptyChatListFilterText + buttonText = strings.ChatList_EmptyChatListEditFilter + case .forum: + text = "No topics here yet" + buttonText = "Create New Topic" + secondaryButtonText = "Show as Messages" } + let string = NSMutableAttributedString(string: text, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor) + let descriptionString = NSAttributedString(string: descriptionText, font: Font.regular(14.0), textColor: theme.list.itemSecondaryTextColor) + self.textNode.attributedText = string self.descriptionNode.attributedText = descriptionString - self.buttonTextNode.attributedText = NSAttributedString(string: isFilter ? strings.ChatList_EmptyChatListEditFilter : strings.ChatList_EmptyChatListNewMessage, font: Font.regular(17.0), textColor: theme.list.itemAccentColor) - + self.buttonNode.title = buttonText + self.secondaryButtonNode.setAttributedTitle(NSAttributedString(string: secondaryButtonText, font: Font.regular(17.0), textColor: theme.list.itemAccentColor), for: .normal) + self.secondaryButtonNode.isHidden = secondaryButtonText.isEmpty + self.activityIndicator.type = .custom(theme.list.itemAccentColor, 22.0, 1.0, false) if let size = self.validLayout { @@ -145,7 +162,6 @@ final class ChatListEmptyNode: ASDisplayNode { self.textNode.isHidden = self.isLoading self.descriptionNode.isHidden = self.isLoading self.buttonNode.isHidden = self.isLoading - self.buttonTextNode.isHidden = self.isLoading self.activityIndicator.isHidden = !self.isLoading } @@ -157,16 +173,18 @@ final class ChatListEmptyNode: ASDisplayNode { let animationSpacing: CGFloat = 24.0 let descriptionSpacing: CGFloat = 8.0 - let buttonSpacing: CGFloat = 24.0 - let buttonSideInset: CGFloat = 16.0 let textSize = self.textNode.updateLayout(CGSize(width: size.width - 40.0, height: size.height)) let descriptionSize = self.descriptionNode.updateLayout(CGSize(width: size.width - 40.0, height: size.height)) + + let buttonSideInset: CGFloat = 16.0 + let buttonWidth = size.width - buttonSideInset * 2.0 + let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition) + let buttonSize = CGSize(width: buttonWidth, height: buttonHeight) - let buttonWidth = min(size.width - buttonSideInset * 2.0, 280.0) - let buttonSize = CGSize(width: buttonWidth, height: 50.0) + let secondaryButtonSize = self.secondaryButtonNode.measure(CGSize(width: buttonWidth, height: .greatestFiniteMagnitude)) - let contentHeight = self.animationSize.height + animationSpacing + textSize.height + buttonSpacing + buttonSize.height + let contentHeight = self.animationSize.height + animationSpacing + textSize.height + buttonSize.height var contentOffset: CGFloat = 0.0 if size.height < contentHeight { contentOffset = -self.animationSize.height - animationSpacing + 44.0 @@ -178,8 +196,7 @@ final class ChatListEmptyNode: ASDisplayNode { let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - self.animationSize.width) / 2.0), y: floor((size.height - contentHeight) / 2.0) + contentOffset), size: self.animationSize) let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: animationFrame.maxY + animationSpacing), size: textSize) - let descpriptionFrame = CGRect(origin: CGPoint(x: floor((size.width - descriptionSize.width) / 2.0), y: textFrame.maxY + descriptionSpacing), size: descriptionSize) - let bottomTextEdge: CGFloat = descpriptionFrame.width.isZero ? textFrame.maxY : descpriptionFrame.maxY + let descriptionFrame = CGRect(origin: CGPoint(x: floor((size.width - descriptionSize.width) / 2.0), y: textFrame.maxY + descriptionSpacing), size: descriptionSize) if !self.animationSize.width.isZero { self.animationNode.updateLayout(size: self.animationSize) @@ -187,19 +204,28 @@ final class ChatListEmptyNode: ASDisplayNode { } transition.updateFrame(node: self.textNode, frame: textFrame) - transition.updateFrame(node: self.descriptionNode, frame: descpriptionFrame) + transition.updateFrame(node: self.descriptionNode, frame: descriptionFrame) - let buttonTextSize = self.buttonTextNode.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) - let buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonTextSize.width) / 2.0), y: bottomTextEdge + buttonSpacing), size: buttonTextSize) + var bottomInset: CGFloat = 16.0 + let secondaryButtonFrame = CGRect(origin: CGPoint(x: floor((size.width - secondaryButtonSize.width) / 2.0), y: size.height - secondaryButtonSize.height - bottomInset), size: secondaryButtonSize) + transition.updateFrame(node: self.secondaryButtonNode, frame: secondaryButtonFrame) + + if secondaryButtonSize.height > 0.0 { + bottomInset += secondaryButtonSize.height + 23.0 + } + + let buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - buttonHeight - bottomInset), size: buttonSize) transition.updateFrame(node: self.buttonNode, frame: buttonFrame) - transition.updateFrame(node: self.buttonTextNode, frame: buttonFrame) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.buttonNode.frame.contains(point) { return self.buttonNode.view.hitTest(self.view.convert(point, to: self.buttonNode.view), with: event) } + if self.secondaryButtonNode.frame.contains(point), !self.secondaryButtonNode.isHidden { + return self.secondaryButtonNode.view.hitTest(self.view.convert(point, to: self.secondaryButtonNode.view), with: event) + } if self.animationNode.frame.contains(point) { return self.animationNode.view.hitTest(self.view.convert(point, to: self.animationNode.view), with: event) } diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 4e2957d9e6..9ee23d9a9e 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -105,6 +105,7 @@ swift_library( "//submodules/InviteLinksUI:InviteLinksUI", "//submodules/HorizontalPeerItem:HorizontalPeerItem", "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", + "//submodules/PersistentStringHash:PersistentStringHash", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/AdditionalLinkItem.swift b/submodules/SettingsUI/Sources/AdditionalLinkItem.swift new file mode 100644 index 0000000000..87701e98a8 --- /dev/null +++ b/submodules/SettingsUI/Sources/AdditionalLinkItem.swift @@ -0,0 +1,497 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import ItemListUI +import ShimmerEffect +import TelegramCore + +public class AdditionalLinkItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let username: TelegramPeerUsername? + public let sectionId: ItemListSectionId + let style: ItemListStyle + let tapAction: (() -> Void)? + public let tag: ItemListItemTag? + + public init( + presentationData: ItemListPresentationData, + username: TelegramPeerUsername?, + sectionId: ItemListSectionId, + style: ItemListStyle, + tapAction: (() -> Void)?, + tag: ItemListItemTag? = nil + ) { + self.presentationData = presentationData + self.username = username + self.sectionId = sectionId + self.style = style + self.tapAction = tapAction + self.tag = tag + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + var firstWithHeader = false + var last = false + if self.style == .plain { + if previousItem == nil { + firstWithHeader = true + } + if nextItem == nil { + last = true + } + } + let node = ItemListInviteLinkItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListInviteLinkItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + var firstWithHeader = false + var last = false + if self.style == .plain { + if previousItem == nil { + firstWithHeader = true + } + if nextItem == nil { + last = true + } + } + + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public var selectable: Bool = true + + public func selected(listView: ListView) { + listView.clearHighlightAnimated(true) + self.tapAction?() + } +} + +public class ItemListInviteLinkItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private let maskNode: ASImageNode + + private let extractedBackgroundImageNode: ASImageNode + + private let containerNode: ContextControllerSourceNode + private let contextSourceNode: ContextExtractedContentContainingNode + + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + + private let offsetContainerNode: ASDisplayNode + + private let iconBackgroundNode: ASImageNode + private let iconNode: ASImageNode + + private let titleNode: TextNode + private let subtitleNode: TextNode + + private var placeholderNode: ShimmerEffectNode? + private var absoluteLocation: (CGRect, CGSize)? + + private var layoutParams: (AdditionalLinkItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)? + + private var reorderControlNode: ItemListEditableReorderControlNode? + + public var tag: ItemListItemTag? + + public init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + self.maskNode = ASImageNode() + + self.extractedBackgroundImageNode = ASImageNode() + self.extractedBackgroundImageNode.displaysAsynchronously = false + self.extractedBackgroundImageNode.alpha = 0.0 + + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.offsetContainerNode = ASDisplayNode() + + self.iconBackgroundNode = ASImageNode() + self.iconBackgroundNode.displaysAsynchronously = false + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.contentMode = .center + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.subtitleNode = TextNode() + self.subtitleNode.isUserInteractionEnabled = false + self.subtitleNode.contentMode = .left + self.subtitleNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.isAccessibilityElement = true + + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) + self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) + + self.offsetContainerNode.addSubnode(self.iconBackgroundNode) + self.offsetContainerNode.addSubnode(self.iconNode) + self.offsetContainerNode.addSubnode(self.titleNode) + self.offsetContainerNode.addSubnode(self.subtitleNode) + } + + public func asyncLayout() -> (_ item: AdditionalLinkItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) + let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) + + let currentItem = self.layoutParams?.0 + + return { item, params, neighbors, firstWithHeader, last in + var updatedTheme: PresentationTheme? + + let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) + let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) + + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let iconColor: UIColor + if let username = item.username { + if username.flags.contains(.isEditable) || username.flags.contains(.isActive) { + iconColor = item.presentationData.theme.list.itemAccentColor + } else { + iconColor = UIColor(rgb: 0xa8b2bb) + } + } else { + iconColor = item.presentationData.theme.list.mediaPlaceholderColor + } + + let titleText: String + let subtitleText: String + if let username = item.username { + titleText = "@\(username.username)" + + if username.flags.contains(.isEditable) || username.flags.contains(.isActive) { + subtitleText = item.presentationData.strings.Group_Setup_LinkActive + } else { + subtitleText = item.presentationData.strings.Group_Setup_LinkInactive + } + } else { + titleText = " " + subtitleText = " " + } + + let titleAttributedString = NSAttributedString(string: titleText, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + let subtitleAttributedString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) + + let reorderControlSizeAndApply = reorderControlLayout(item.presentationData.theme) + let reorderInset: CGFloat = reorderControlSizeAndApply.0 + + let leftInset: CGFloat = 65.0 + params.leftInset + let rightInset: CGFloat = 16.0 + params.rightInset + let verticalInset: CGFloat = subtitleAttributedString.string.isEmpty ? 14.0 : 8.0 + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - reorderInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - reorderInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let titleSpacing: CGFloat = 1.0 + + let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0 + let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + + var insets: UIEdgeInsets + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + insets = itemListNeighborsPlainInsets(neighbors) + insets.top = firstWithHeader ? 29.0 : 0.0 + insets.bottom = 0.0 + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + insets = itemListNeighborsGroupedInsets(neighbors, params) + } + + let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight)) + let separatorHeight = UIScreenPixel + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.layoutParams = (item, params, neighbors, firstWithHeader, last) + + strongSelf.accessibilityLabel = titleAttributedString.string + strongSelf.accessibilityValue = subtitleAttributedString.string + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.containerNode.isGestureEnabled = false + + let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height)) + let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0) + strongSelf.extractedRect = extractedRect + strongSelf.nonExtractedRect = nonExtractedRect + + if strongSelf.contextSourceNode.isExtractedToContextPreview { + strongSelf.extractedBackgroundImageNode.frame = extractedRect + } else { + strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect + } + strongSelf.contextSourceNode.contentRect = extractedRect + + strongSelf.iconBackgroundNode.image = generateFilledCircleImage(diameter: 40.0, color: iconColor) + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor) + } + + let transition = ContainedViewLayoutTransition.immediate + + let _ = titleApply() + let _ = subtitleApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + + let stripeInset: CGFloat + if case .none = neighbors.bottom { + stripeInset = 0.0 + } else { + stripeInset = leftInset + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: stripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - stripeInset, height: separatorHeight)) + strongSelf.bottomStripeNode.isHidden = last + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + let iconSize: CGSize = CGSize(width: 40.0, height: 40.0) + let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 12.0, y: floorToScreenPixels((layout.contentSize.height - iconSize.height) / 2.0)), size: iconSize) + strongSelf.iconBackgroundNode.bounds = CGRect(origin: CGPoint(), size: iconSize) + strongSelf.iconBackgroundNode.position = iconFrame.center + strongSelf.iconNode.frame = iconFrame + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) + + if strongSelf.reorderControlNode == nil { + let reorderControlNode = reorderControlSizeAndApply.1(layout.contentSize.height, false, .immediate) + strongSelf.reorderControlNode = reorderControlNode + strongSelf.addSubnode(reorderControlNode) + reorderControlNode.alpha = 0.0 + transition.updateAlpha(node: reorderControlNode, alpha: 1.0) + } + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - reorderControlSizeAndApply.0, y: 0.0), size: CGSize(width: reorderControlSizeAndApply.0, height: layout.contentSize.height)) + strongSelf.reorderControlNode?.frame = reorderControlFrame + + if item.username == nil { + let shimmerNode: ShimmerEffectNode + if let current = strongSelf.placeholderNode { + shimmerNode = current + } else { + shimmerNode = ShimmerEffectNode() + strongSelf.placeholderNode = shimmerNode + strongSelf.addSubnode(shimmerNode) + } + shimmerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + if let (rect, size) = strongSelf.absoluteLocation { + shimmerNode.updateAbsoluteRect(rect, within: size) + } + + var shapes: [ShimmerEffectNode.Shape] = [] + + let titleLineWidth: CGFloat = 180.0 + let subtitleLineWidth: CGFloat = 60.0 + let lineDiameter: CGFloat = 10.0 + + let iconFrame = strongSelf.iconBackgroundNode.frame + shapes.append(.circle(iconFrame)) + + let titleFrame = strongSelf.titleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floor((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter)) + + let subtitleFrame = strongSelf.subtitleNode.frame + shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: lineDiameter)) + + shimmerNode.update(backgroundColor: item.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize) + } else if let shimmerNode = strongSelf.placeholderNode { + strongSelf.placeholderNode = nil + shimmerNode.removeFromSupernode() + } + } + }) + } + } + + override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + var rect = rect + rect.origin.y += self.insets.top + self.absoluteLocation = (rect, containerSize) + if let shimmerNode = self.placeholderNode { + shimmerNode.updateAbsoluteRect(rect, within: containerSize) + } + } + + override public func isReorderable(at point: CGPoint) -> Bool { + if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) { + return true + } + return false + } +} diff --git a/submodules/SettingsUI/Sources/UsernameSetupController.swift b/submodules/SettingsUI/Sources/UsernameSetupController.swift index b2d5c3341a..90ce0726c1 100644 --- a/submodules/SettingsUI/Sources/UsernameSetupController.swift +++ b/submodules/SettingsUI/Sources/UsernameSetupController.swift @@ -17,10 +17,15 @@ private final class UsernameSetupControllerArguments { let updatePublicLinkText: (String?, String) -> Void let shareLink: () -> Void - init(account: Account, updatePublicLinkText: @escaping (String?, String) -> Void, shareLink: @escaping () -> Void) { + let activateLink: (String) -> Void + let deactivateLink: (String) -> Void + + init(account: Account, updatePublicLinkText: @escaping (String?, String) -> Void, shareLink: @escaping () -> Void, activateLink: @escaping (String) -> Void, deactivateLink: @escaping (String) -> Void) { self.account = account self.updatePublicLinkText = updatePublicLinkText self.shareLink = shareLink + self.activateLink = activateLink + self.deactivateLink = deactivateLink } } @@ -49,7 +54,7 @@ private enum UsernameSetupEntry: ItemListNodeEntry { case publicLinkInfo(PresentationTheme, String) case additionalLinkHeader(PresentationTheme, String) - case additionalLink(PresentationTheme, String, Bool, Int32) + case additionalLink(PresentationTheme, TelegramPeerUsername, Int32) case additionalLinkInfo(PresentationTheme, String) var section: ItemListSectionId { @@ -73,7 +78,7 @@ private enum UsernameSetupEntry: ItemListNodeEntry { return 3 case .additionalLinkHeader: return 4 - case let .additionalLink(_, _, _, index): + case let .additionalLink(_, _, index): return 5 + index case .additionalLinkInfo: return 1000 @@ -112,8 +117,8 @@ private enum UsernameSetupEntry: ItemListNodeEntry { } else { return false } - case let .additionalLink(lhsTheme, lhsLink, lhsActive, lhsIndex): - if case let .additionalLink(rhsTheme, rhsLink, rhsActive, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsLink == rhsLink, lhsActive == rhsActive, lhsIndex == rhsIndex { + case let .additionalLink(lhsTheme, lhsAddressName, lhsIndex): + if case let .additionalLink(rhsTheme, rhsAddressName, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsIndex == rhsIndex { return true } else { return false @@ -167,8 +172,16 @@ private enum UsernameSetupEntry: ItemListNodeEntry { return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: string, sectionId: self.section) case let .additionalLinkHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .additionalLink(_, link, _, _): - return ItemListTextItem(presentationData: presentationData, text: .plain(link), sectionId: self.section) + case let .additionalLink(_, link, _): + return AdditionalLinkItem(presentationData: presentationData, username: link, sectionId: self.section, style: .blocks, tapAction: { + if !link.flags.contains(.isEditable) { + if link.flags.contains(.isActive) { + arguments.deactivateLink(link.username) + } else { + arguments.activateLink(link.username) + } + } + }) case let .additionalLinkInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } @@ -219,7 +232,7 @@ private struct UsernameSetupControllerState: Equatable { } } -private func usernameSetupControllerEntries(presentationData: PresentationData, view: PeerView, state: UsernameSetupControllerState) -> [UsernameSetupEntry] { +private func usernameSetupControllerEntries(presentationData: PresentationData, view: PeerView, state: UsernameSetupControllerState, temporaryOrder: [String]?) -> [UsernameSetupEntry] { var entries: [UsernameSetupEntry] = [] if let peer = view.peers[view.peerId] as? TelegramUser { @@ -280,6 +293,27 @@ private func usernameSetupControllerEntries(presentationData: PresentationData, if !otherUsernames.isEmpty { entries.append(.additionalLinkHeader(presentationData.theme, presentationData.strings.Username_LinksOrder)) + + var usernames = peer.usernames + if let temporaryOrder = temporaryOrder { + var usernamesMap: [String: TelegramPeerUsername] = [:] + for username in usernames { + usernamesMap[username.username] = username + } + var sortedUsernames: [TelegramPeerUsername] = [] + for username in temporaryOrder { + if let username = usernamesMap[username] { + sortedUsernames.append(username) + } + } + usernames = sortedUsernames + } + var i: Int32 = 0 + for username in usernames { + entries.append(.additionalLink(presentationData.theme, username, i)) + i += 1 + } + entries.append(.additionalLinkInfo(presentationData.theme, presentationData.strings.Username_LinksOrderInfo)) } } @@ -350,76 +384,194 @@ public func usernameSetupController(context: AccountContext) -> ViewController { presentControllerImpl?(shareController, nil) } }) + }, activateLink: { name in + dismissInputImpl?() + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_ActivateAlertTitle, text: presentationData.strings.Username_ActivateAlertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_ActivateAlertShow, action: { + let _ = context.engine.peers.toggleAddressNameActive(domain: .account, name: name, active: true).start() + })]), nil) + }, deactivateLink: { name in + dismissInputImpl?() + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Username_DeactivateAlertTitle, text: presentationData.strings.Username_DeactivateAlertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Username_DeactivateAlertHide, action: { + let _ = context.engine.peers.toggleAddressNameActive(domain: .account, name: name, active: false).start() + })]), nil) }) + let temporaryOrder = Promise<[String]?>(nil) + let peerView = context.account.viewTracker.peerView(context.account.peerId) |> deliverOnMainQueue - let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, peerView) - |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, Any)) in - let peer = peerViewMainPeer(view) + let signal = combineLatest( + context.sharedContext.presentationData, + statePromise.get() |> deliverOnMainQueue, + peerView, + temporaryOrder.get() + ) + |> map { presentationData, state, view, temporaryOrder -> (ItemListControllerState, (ItemListNodeState, Any)) in + let peer = peerViewMainPeer(view) + + var rightNavigationButton: ItemListNavigationButton? + if let peer = peer as? TelegramUser { + var doneEnabled = true - var rightNavigationButton: ItemListNavigationButton? - if let peer = peer as? TelegramUser { - var doneEnabled = true - - if let addressNameValidationStatus = state.addressNameValidationStatus { - switch addressNameValidationStatus { - case .availability(.available): - break - default: - doneEnabled = false + if let addressNameValidationStatus = state.addressNameValidationStatus { + switch addressNameValidationStatus { + case .availability(.available): + break + default: + doneEnabled = false + } + } + + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + var updatedAddressNameValue: String? + updateState { state in + if state.editingPublicLinkText != peer.addressName { + updatedAddressNameValue = state.editingPublicLinkText + } + + if updatedAddressNameValue != nil { + return state.withUpdatedUpdatingAddressName(true) + } else { + return state } } - rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { - var updatedAddressNameValue: String? - updateState { state in - if state.editingPublicLinkText != peer.addressName { - updatedAddressNameValue = state.editingPublicLinkText + if let updatedAddressNameValue = updatedAddressNameValue { + updateAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: .account, name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) + |> deliverOnMainQueue).start(error: { _ in + updateState { state in + return state.withUpdatedUpdatingAddressName(false) + } + }, completed: { + updateState { state in + return state.withUpdatedUpdatingAddressName(false) } - if updatedAddressNameValue != nil { - return state.withUpdatedUpdatingAddressName(true) - } else { - return state - } - } - - if let updatedAddressNameValue = updatedAddressNameValue { - updateAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: .account, name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) - |> deliverOnMainQueue).start(error: { _ in - updateState { state in - return state.withUpdatedUpdatingAddressName(false) - } - }, completed: { - updateState { state in - return state.withUpdatedUpdatingAddressName(false) - } - - dismissImpl?() - })) - } else { dismissImpl?() - } - }) - } - - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { - dismissImpl?() + })) + } else { + dismissImpl?() + } }) + } + + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + dismissImpl?() + }) + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Username_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: usernameSetupControllerEntries(presentationData: presentationData, view: view, state: state, temporaryOrder: temporaryOrder), style: .blocks, focusItemTag: UsernameEntryTag.username, animateChanges: true) - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Username_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: usernameSetupControllerEntries(presentationData: presentationData, view: view, state: state), style: .blocks, focusItemTag: UsernameEntryTag.username, animateChanges: false) - - return (controllerState, (listState, arguments)) - } |> afterDisposed { - actionsDisposable.dispose() + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() } let controller = ItemListController(context: context, state: signal) controller.navigationPresentation = .modal controller.enableInteractiveDismiss = true + + controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [UsernameSetupEntry]) -> Signal in + let fromEntry = entries[fromIndex] + guard case let .additionalLink(_, fromUsername, _) = fromEntry else { + return .single(false) + } + var referenceId: String? + var beforeAll = false + var afterAll = false + if toIndex < entries.count { + switch entries[toIndex] { + case let .additionalLink(_, toUsername, _): + referenceId = toUsername.username + default: + if entries[toIndex] < fromEntry { + beforeAll = true + } else { + afterAll = true + } + } + } else { + afterAll = true + } + + var currentUsernames: [String] = [] + for entry in entries { + switch entry { + case let .additionalLink(_, link, _): + currentUsernames.append(link.username) + default: + break + } + } + + var previousIndex: Int? + for i in 0 ..< currentUsernames.count { + if currentUsernames[i] == fromUsername.username { + previousIndex = i + currentUsernames.remove(at: i) + break + } + } + + var didReorder = false + + if let referenceId = referenceId { + var inserted = false + for i in 0 ..< currentUsernames.count { + if currentUsernames[i] == referenceId { + if fromIndex < toIndex { + didReorder = previousIndex != i + 1 + currentUsernames.insert(fromUsername.username, at: i + 1) + } else { + didReorder = previousIndex != i + currentUsernames.insert(fromUsername.username, at: i) + } + inserted = true + break + } + } + if !inserted { + didReorder = previousIndex != currentUsernames.count + currentUsernames.append(fromUsername.username) + } + } else if beforeAll { + didReorder = previousIndex != 0 + currentUsernames.insert(fromUsername.username, at: 0) + } else if afterAll { + didReorder = previousIndex != currentUsernames.count + currentUsernames.append(fromUsername.username) + } + + temporaryOrder.set(.single(currentUsernames)) + + if didReorder { + DispatchQueue.main.async { + dismissInputImpl?() + } + } + + return .single(didReorder) + }) + + controller.setReorderCompleted({ (entries: [UsernameSetupEntry]) -> Void in + var currentUsernames: [TelegramPeerUsername] = [] + for entry in entries { + switch entry { + case let .additionalLink(_, username, _): + currentUsernames.append(username) + default: + break + } + } + let _ = (context.engine.peers.reorderAddressNames(domain: .account, names: currentUsernames) + |> deliverOnMainQueue).start(completed: { + temporaryOrder.set(.single(nil)) + }) + }) + dismissImpl = { [weak controller] in controller?.view.endEditing(true) controller?.dismiss() diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 123fd603b8..0a5cbe9c90 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -104,11 +104,11 @@ private final class TitleFieldComponent: Component { self.component = component self.state = state - let titleCredibilityContent: EmojiStatusComponent.Content + let iconContent: EmojiStatusComponent.Content if component.fileId == 0 { - titleCredibilityContent = .topic(title: String(component.text.prefix(1))) + iconContent = .topic(title: String(component.text.prefix(1))) } else { - titleCredibilityContent = .animation(content: .customEmoji(fileId: component.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: component.placeholderColor, themeColor: component.accentColor, loopMode: .count(2)) + iconContent = .animation(content: .customEmoji(fileId: component.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: component.placeholderColor, themeColor: component.accentColor, loopMode: .count(2)) } let iconSize = self.iconView.update( @@ -117,7 +117,7 @@ private final class TitleFieldComponent: Component { context: component.context, animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, - content: titleCredibilityContent, + content: iconContent, isVisibleForAnimations: true, action: nil )), @@ -314,12 +314,14 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { let peerId: EnginePeer.Id let mode: ForumCreateTopicScreen.Mode let titleUpdated: (String) -> Void + let iconUpdated: (Int64?) -> Void - init(context: AccountContext, peerId: EnginePeer.Id, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void) { + init(context: AccountContext, peerId: EnginePeer.Id, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void) { self.context = context self.peerId = peerId self.mode = mode self.titleUpdated = titleUpdated + self.iconUpdated = iconUpdated } static func ==(lhs: ForumCreateTopicScreenComponent, rhs: ForumCreateTopicScreenComponent) -> Bool { @@ -338,6 +340,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext private let titleUpdated: (String) -> Void + private let iconUpdated: (Int64?) -> Void var emojiContent: EmojiPagerContentComponent? private let emojiContentDisposable = MetaDisposable() @@ -345,9 +348,10 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { var title: String var fileId: Int64 - init(context: AccountContext, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void) { + init(context: AccountContext, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void) { self.context = context self.titleUpdated = titleUpdated + self.iconUpdated = iconUpdated switch mode { case .create: @@ -422,6 +426,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { self.fileId = item.itemFile?.fileId.id ?? 0 self.updated(transition: .immediate) + self.iconUpdated(self.fileId != 0 ? self.fileId : nil) + self.emojiContentDisposable.set(( EmojiPagerContentComponent.emojiInputData( context: self.context, @@ -449,7 +455,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent { return State( context: self.context, mode: self.mode, - titleUpdated: self.titleUpdated + titleUpdated: self.titleUpdated, + iconUpdated: self.iconUpdated ) } @@ -666,10 +673,16 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { case edit(topic: ForumChannelTopics.Item) } + private var state: (String, Int64?) = ("", nil) + public var completion: (String, Int64?) -> Void = { _, _ in } + public init(context: AccountContext, peerId: EnginePeer.Id, mode: ForumCreateTopicScreen.Mode) { var titleUpdatedImpl: ((String) -> Void)? + var iconUpdatedImpl: ((Int64?) -> Void)? super.init(context: context, component: ForumCreateTopicScreenComponent(context: context, peerId: peerId, mode: mode, titleUpdated: { title in titleUpdatedImpl?(title) + }, iconUpdated: { fileId in + iconUpdatedImpl?(fileId) }), navigationBarAppearance: .transparent) let title: String @@ -692,7 +705,20 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { self.navigationItem.rightBarButtonItem?.isEnabled = false titleUpdatedImpl = { [weak self] title in - self?.navigationItem.rightBarButtonItem?.isEnabled = !title.isEmpty + guard let strongSelf = self else { + return + } + strongSelf.navigationItem.rightBarButtonItem?.isEnabled = !title.isEmpty + + strongSelf.state = (title, strongSelf.state.1) + } + + iconUpdatedImpl = { [weak self] fileId in + guard let strongSelf = self else { + return + } + + strongSelf.state = (strongSelf.state.0, fileId) } } @@ -706,5 +732,7 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { @objc private func createPressed() { self.dismiss() + + self.completion(self.state.0, self.state.1) } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 3826c93ab3..d2def218ad 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -317,6 +317,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var editingUrlPreviewQueryState: (String?, Disposable)? private var searchState: ChatSearchState? + private var shakeFeedback: HapticFeedback? + private var recordingModeFeedback: HapticFeedback? private var recorderFeedback: HapticFeedback? private var audioRecorderValue: ManagedAudioRecorder? @@ -15510,6 +15512,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .id(messageId) = messageSubject { strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, timecode)) } + } else { + self?.playShakeAnimation() } } else if let navigationController = strongSelf.effectiveNavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peerId), subject: subject, keepStack: .always, peekData: peekData)) @@ -16975,6 +16979,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } } + + public func playShakeAnimation() { + if self.shakeFeedback == nil { + self.shakeFeedback = HapticFeedback() + } + self.shakeFeedback?.error() + + self.chatDisplayNode.historyNodeContainer.layer.addShakeAnimation(amplitude: -6.0, decay: true) + } } private final class ContextControllerContentSourceImpl: ContextControllerContentSource { diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index c6b577c852..f2e54e3479 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -12,6 +12,8 @@ import TelegramStringFormatting import AccountContext import ChatPresentationInterfaceState import WallpaperBackgroundNode +import ComponentFlow +import EmojiStatusComponent private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize @@ -775,13 +777,128 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC } } -private enum ChatEmptyNodeContentType { +final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { + private let context: AccountContext + private let fileId: Int64? + + private let titleNode: ImmediateTextNode + private let textNode: ImmediateTextNode + + private var currentTheme: PresentationTheme? + private var currentStrings: PresentationStrings? + + private let iconView: ComponentView + + init(context: AccountContext, fileId: Int64?) { + self.context = context + self.fileId = fileId + + self.titleNode = ImmediateTextNode() + self.titleNode.maximumNumberOfLines = 0 + self.titleNode.lineSpacing = 0.15 + self.titleNode.textAlignment = .center + self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false + + self.textNode = ImmediateTextNode() + self.textNode.maximumNumberOfLines = 0 + self.textNode.lineSpacing = 0.15 + self.textNode.textAlignment = .center + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + + self.iconView = ComponentView() + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + } + + func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) + if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { + self.currentTheme = interfaceState.theme + self.currentStrings = interfaceState.strings + + self.titleNode.attributedText = NSAttributedString(string: "Almost done!", font: titleFont, textColor: serviceColor.primaryText) + + self.textNode.attributedText = NSAttributedString(string: "Send first message to\nstart this topic.", font: messageFont, textColor: serviceColor.primaryText) + } + + let inset: CGFloat + if size.width == 320.0 { + inset = 8.0 + } else { + inset = 15.0 + } + + let title = "" + let iconContent: EmojiStatusComponent.Content + if let fileId = self.fileId { + iconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: .clear, themeColor: serviceColor.primaryText, loopMode: .count(2)) + } else { + iconContent = .topic(title: String(title.prefix(1))) + } + + let insets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) + let titleSpacing: CGFloat = 6.0 + let iconSpacing: CGFloat = 9.0 + + let iconSize = self.iconView.update( + transition: .easeInOut(duration: 0.2), + component: AnyComponent(EmojiStatusComponent( + context: self.context, + animationCache: self.context.animationCache, + animationRenderer: self.context.animationRenderer, + content: iconContent, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: 54.0, height: 54.0) + ) + + var contentWidth: CGFloat = 196.0 + var contentHeight: CGFloat = 0.0 + + let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) + let textSize = self.textNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) + + contentWidth = max(contentWidth, max(titleSize.width, textSize.width)) + + contentHeight += titleSize.height + titleSpacing + textSize.height + iconSpacing + iconSize.height + + let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) + + let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - iconSize.width) / 2.0), y: contentRect.minY), size: iconSize) + + if let iconComponentView = self.iconView.view { + if iconComponentView.superview == nil { + self.view.addSubview(iconComponentView) + } + transition.updateFrame(view: iconComponentView, frame: iconFrame) + } + + let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleSize) + transition.updateFrame(node: self.titleNode, frame: titleFrame) + + let textFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size + } +} + + +private enum ChatEmptyNodeContentType: Equatable { case regular case secret case group case cloud case peerNearby case greeting + case topic(Int64?) } final class ChatEmptyNode: ASDisplayNode { @@ -857,9 +974,13 @@ final class ChatEmptyNode: ASDisplayNode { let contentType: ChatEmptyNodeContentType if case .replyThread = interfaceState.chatLocation { - contentType = .regular + if case .topic = emptyType { + contentType = .topic(nil) + } else { + contentType = .regular + } } else if let peer = interfaceState.renderedPeer?.peer, !isScheduledMessages { - if peer.id == self.context.account.peerId { + if peer.id == self.context.account.peerId { contentType = .cloud } else if let _ = peer as? TelegramSecretChat { contentType = .secret @@ -890,7 +1011,7 @@ final class ChatEmptyNode: ASDisplayNode { var animateContentIn = false if let node = self.content?.1 { node.removeFromSupernode() - if self.content?.0 != nil && contentType == .greeting && transition.isAnimated { + if self.content?.0 != nil, case .greeting = contentType, transition.isAnimated { animateContentIn = true } } @@ -909,6 +1030,8 @@ final class ChatEmptyNode: ASDisplayNode { case .greeting: node = ChatEmptyNodeGreetingChatContent(context: self.context, interaction: self.interaction) updateGreetingSticker = true + case .topic: + node = ChatEmptyNodeTopicChatContent(context: self.context, fileId: nil) } self.content = (contentType, node) self.addSubnode(node) diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 92861c43bc..bb4c7553a4 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -75,7 +75,7 @@ func chatHistoryEntriesForView( associatedMedia: [:] ) } - + var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = [] var count = 0 loop: for entry in view.entries { @@ -211,6 +211,19 @@ func chatHistoryEntriesForView( let topMessage = messages[0] + var hasTopicCreated = false + inner: for media in topMessage.media { + if let action = media as? TelegramMediaAction { + switch action.action { + case .topicCreated: + hasTopicCreated = true + break inner + default: + break + } + } + } + var adminRank: CachedChannelAdminRank? if let author = topMessage.author { adminRank = adminRanks[author.id] @@ -235,12 +248,18 @@ func chatHistoryEntriesForView( } entries.insert(.MessageGroupEntry(groupInfo, groupMessages, presentationData), at: 0) } else { - entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false)), at: 0) + if !hasTopicCreated { + entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false)), at: 0) + } } let replyCount = view.entries.isEmpty ? 0 : 1 - entries.insert(.ReplyCountEntry(messages[0].index, replyThreadMessage.isChannelPost, replyCount, presentationData), at: 1) + if hasTopicCreated && replyCount == 0 { + + } else { + entries.insert(.ReplyCountEntry(messages[0].index, replyThreadMessage.isChannelPost, replyCount, presentationData), at: 1) + } } break loop default: diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 4ac8164acf..c6eea5ff4e 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2527,12 +2527,36 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } else if action.action == .historyCleared { emptyType = .clearedHistory break + } else if case .topicCreated = action.action { + emptyType = .topic + break } } } loadState = .empty(emptyType) } else { - loadState = .empty(.generic) + var emptyType = ChatHistoryNodeLoadState.EmptyType.generic + if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation { + loop: for entry in historyView.originalView.additionalData { + switch entry { + case let .message(id, messages) where id == replyThreadMessage.effectiveTopId: + if let message = messages.first { + for media in message.media { + if let action = media as? TelegramMediaAction { + if case .topicCreated = action.action { + emptyType = .topic + break + } + } + } + break loop + } + default: + break + } + } + } + loadState = .empty(emptyType) } } else { loadState = .messages diff --git a/submodules/TelegramUI/Sources/ChatHistoryNode.swift b/submodules/TelegramUI/Sources/ChatHistoryNode.swift index d30323ee12..b9a76f29b2 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryNode.swift @@ -11,6 +11,7 @@ public enum ChatHistoryNodeLoadState: Equatable { case generic case joined case clearedHistory + case topic } case loading diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 6d630dbb31..f1fee6ab99 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -951,8 +951,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) } if let username = user.username { + let mainUsername = user.usernames.first?.username ?? username var additionalUsernames: String? - let usernames = user.usernames.filter { !$0.flags.contains(.isEditable) && $0.flags.contains(.isActive) } + let usernames = user.usernames.filter { $0.flags.contains(.isActive) && $0.username != mainUsername } if !usernames.isEmpty { additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string } @@ -961,7 +962,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese PeerInfoScreenLabeledValueItem( id: 1, label: presentationData.strings.Profile_Username, - text: "@\(username)", + text: "@\(mainUsername)", additionalText: additionalUsernames, textColor: .accent, icon: .qrCode, @@ -1083,17 +1084,18 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if let username = channel.username { var additionalUsernames: String? - let usernames = channel.usernames.filter { !$0.flags.contains(.isEditable) && $0.flags.contains(.isActive) } + let mainUsername = channel.usernames.first?.username ?? username + + let usernames = channel.usernames.filter { $0.flags.contains(.isActive) && $0.username != mainUsername } if !usernames.isEmpty { additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string } - items[.peerInfo]!.append( PeerInfoScreenLabeledValueItem( id: ItemUsername, label: presentationData.strings.Channel_LinkItem, - text: "https://t.me/\(username)", + text: "https://t.me/\(mainUsername)", additionalText: additionalUsernames, textColor: .accent, icon: .qrCode,