mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Various improvements
This commit is contained in:
parent
533085c77a
commit
308bc5ad6a
@ -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";
|
||||
|
@ -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 }
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -105,6 +105,7 @@ swift_library(
|
||||
"//submodules/InviteLinksUI:InviteLinksUI",
|
||||
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
||||
"//submodules/PersistentStringHash:PersistentStringHash",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
497
submodules/SettingsUI/Sources/AdditionalLinkItem.swift
Normal file
497
submodules/SettingsUI/Sources/AdditionalLinkItem.swift
Normal file
@ -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<Void, NoError>?, (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
|
||||
}
|
||||
}
|
@ -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<Bool, NoError> 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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<Empty>
|
||||
|
||||
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<Empty>()
|
||||
|
||||
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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -11,6 +11,7 @@ public enum ChatHistoryNodeLoadState: Equatable {
|
||||
case generic
|
||||
case joined
|
||||
case clearedHistory
|
||||
case topic
|
||||
}
|
||||
|
||||
case loading
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user