Various improvements

This commit is contained in:
Ilya Laktyushin 2022-10-04 17:20:21 +03:00
parent 533085c77a
commit 308bc5ad6a
14 changed files with 1080 additions and 131 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -105,6 +105,7 @@ swift_library(
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/PersistentStringHash:PersistentStringHash",
],
visibility = [
"//visibility:public",

View 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ public enum ChatHistoryNodeLoadState: Equatable {
case generic
case joined
case clearedHistory
case topic
}
case loading

View File

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