mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 11:23:48 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
01a7255c0b
@ -8097,6 +8097,14 @@ Sorry for the inconvenience.";
|
|||||||
"Username.LinksOrder" = "USERNAMES ORDER";
|
"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.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 %@";
|
"Profile.AdditionalUsernames" = "also %@";
|
||||||
|
|
||||||
"EmojiSearch.SearchReactionsPlaceholder" = "Search Reactions";
|
"EmojiSearch.SearchReactionsPlaceholder" = "Search Reactions";
|
||||||
|
@ -733,7 +733,7 @@ public protocol SharedAccountContext: AnyObject {
|
|||||||
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
|
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
|
||||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||||
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
|
||||||
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, navigationController: NavigationController) -> Signal<Never, NoError>
|
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, navigationController: NavigationController, activateInput: ChatControllerActivateInput?) -> Signal<Never, NoError>
|
||||||
func openStorageUsage(context: AccountContext)
|
func openStorageUsage(context: AccountContext)
|
||||||
func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController)
|
func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController)
|
||||||
func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void)
|
func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void)
|
||||||
|
@ -1314,7 +1314,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
strongSelf.context.sharedContext.navigateToForumChannel(context: strongSelf.context, peerId: channel.id, navigationController: navigationController)
|
strongSelf.context.sharedContext.navigateToForumChannel(context: strongSelf.context, peerId: channel.id, navigationController: navigationController)
|
||||||
} else {
|
} else {
|
||||||
if let threadId = threadId {
|
if let threadId = threadId {
|
||||||
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, navigationController: navigationController).start()
|
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, navigationController: navigationController, activateInput: nil).start()
|
||||||
strongSelf.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
|
strongSelf.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
|
||||||
} else {
|
} else {
|
||||||
var navigationAnimationOptions: NavigationAnimationOptions = []
|
var navigationAnimationOptions: NavigationAnimationOptions = []
|
||||||
@ -1520,13 +1520,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.chatListDisplayNode.emptyListAction = { [weak self] in
|
self.chatListDisplayNode.emptyListAction = { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter {
|
if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter {
|
||||||
strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in }))
|
strongSelf.push(chatListFilterPresetController(context: strongSelf.context, currentPreset: filter, updated: { _ in }))
|
||||||
} else {
|
} 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, activateInput: .text).start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
strongSelf.push(controller)
|
||||||
|
} else {
|
||||||
|
strongSelf.composePressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2499,10 +2512,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}, action: { action in
|
}, action: { action in
|
||||||
action.dismissWithResult(.default)
|
action.dismissWithResult(.default)
|
||||||
|
|
||||||
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: "Topic#\(Int.random(in: 0 ..< 100000)) very long title to fill two lines", iconFileId: nil)
|
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create)
|
||||||
|> deliverOnMainQueue).start(next: { topicId in
|
controller.navigationPresentation = .modal
|
||||||
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()
|
controller.completion = { title, fileId in
|
||||||
})
|
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconFileId: fileId)
|
||||||
|
|> deliverOnMainQueue).start(next: { topicId in
|
||||||
|
if let navigationController = (sourceController.navigationController as? NavigationController) {
|
||||||
|
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, navigationController: navigationController, activateInput: .text).start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sourceController.push(controller)
|
||||||
})))
|
})))
|
||||||
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
@ -286,6 +286,7 @@ private final class ChatListContainerItemNode: ASDisplayNode {
|
|||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private let becameEmpty: (ChatListFilter?) -> Void
|
private let becameEmpty: (ChatListFilter?) -> Void
|
||||||
private let emptyAction: (ChatListFilter?) -> Void
|
private let emptyAction: (ChatListFilter?) -> Void
|
||||||
|
private let secondaryEmptyAction: () -> Void
|
||||||
|
|
||||||
private var floatingHeaderOffset: CGFloat?
|
private var floatingHeaderOffset: CGFloat?
|
||||||
|
|
||||||
@ -296,13 +297,14 @@ private final class ChatListContainerItemNode: ASDisplayNode {
|
|||||||
|
|
||||||
private var validLayout: (CGSize, UIEdgeInsets, CGFloat)?
|
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.context = context
|
||||||
self.animationCache = animationCache
|
self.animationCache = animationCache
|
||||||
self.animationRenderer = animationRenderer
|
self.animationRenderer = animationRenderer
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.becameEmpty = becameEmpty
|
self.becameEmpty = becameEmpty
|
||||||
self.emptyAction = emptyAction
|
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)
|
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 {
|
if let currentNode = strongSelf.emptyNode {
|
||||||
currentNode.updateIsLoading(isLoading)
|
currentNode.updateIsLoading(isLoading)
|
||||||
} else {
|
} 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)
|
self?.emptyAction(filter)
|
||||||
|
}, secondaryAction: {
|
||||||
|
self?.secondaryEmptyAction()
|
||||||
})
|
})
|
||||||
strongSelf.emptyNode = emptyNode
|
strongSelf.emptyNode = emptyNode
|
||||||
strongSelf.addSubnode(emptyNode)
|
strongSelf.addSubnode(emptyNode)
|
||||||
@ -431,6 +446,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
private let controlsHistoryPreload: Bool
|
private let controlsHistoryPreload: Bool
|
||||||
private let filterBecameEmpty: (ChatListFilter?) -> Void
|
private let filterBecameEmpty: (ChatListFilter?) -> Void
|
||||||
private let filterEmptyAction: (ChatListFilter?) -> Void
|
private let filterEmptyAction: (ChatListFilter?) -> Void
|
||||||
|
private let secondaryEmptyAction: () -> Void
|
||||||
|
|
||||||
fileprivate var onFilterSwitch: (() -> Void)?
|
fileprivate var onFilterSwitch: (() -> Void)?
|
||||||
|
|
||||||
@ -591,12 +607,13 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
var didBeginSelectingChats: (() -> Void)?
|
var didBeginSelectingChats: (() -> Void)?
|
||||||
var displayFilterLimit: (() -> 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.context = context
|
||||||
self.location = location
|
self.location = location
|
||||||
self.previewing = previewing
|
self.previewing = previewing
|
||||||
self.filterBecameEmpty = filterBecameEmpty
|
self.filterBecameEmpty = filterBecameEmpty
|
||||||
self.filterEmptyAction = filterEmptyAction
|
self.filterEmptyAction = filterEmptyAction
|
||||||
|
self.secondaryEmptyAction = secondaryEmptyAction
|
||||||
self.controlsHistoryPreload = controlsHistoryPreload
|
self.controlsHistoryPreload = controlsHistoryPreload
|
||||||
|
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
@ -611,6 +628,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
self?.filterBecameEmpty(filter)
|
self?.filterBecameEmpty(filter)
|
||||||
}, emptyAction: { [weak self] filter in
|
}, emptyAction: { [weak self] filter in
|
||||||
self?.filterEmptyAction(filter)
|
self?.filterEmptyAction(filter)
|
||||||
|
}, secondaryEmptyAction: { [weak self] in
|
||||||
|
self?.secondaryEmptyAction()
|
||||||
})
|
})
|
||||||
self.itemNodes[.all] = itemNode
|
self.itemNodes[.all] = itemNode
|
||||||
self.addSubnode(itemNode)
|
self.addSubnode(itemNode)
|
||||||
@ -887,6 +906,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
self?.filterBecameEmpty(filter)
|
self?.filterBecameEmpty(filter)
|
||||||
}, emptyAction: { [weak self] filter in
|
}, emptyAction: { [weak self] filter in
|
||||||
self?.filterEmptyAction(filter)
|
self?.filterEmptyAction(filter)
|
||||||
|
}, secondaryEmptyAction: { [weak self] in
|
||||||
|
self?.secondaryEmptyAction()
|
||||||
})
|
})
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
self.pendingItemNode = (id, itemNode, disposable)
|
self.pendingItemNode = (id, itemNode, disposable)
|
||||||
@ -1015,6 +1036,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
self?.filterBecameEmpty(filter)
|
self?.filterBecameEmpty(filter)
|
||||||
}, emptyAction: { [weak self] filter in
|
}, emptyAction: { [weak self] filter in
|
||||||
self?.filterEmptyAction(filter)
|
self?.filterEmptyAction(filter)
|
||||||
|
}, secondaryEmptyAction: { [weak self] in
|
||||||
|
self?.secondaryEmptyAction()
|
||||||
})
|
})
|
||||||
self.itemNodes[id] = itemNode
|
self.itemNodes[id] = itemNode
|
||||||
}
|
}
|
||||||
@ -1118,10 +1141,13 @@ final class ChatListControllerNode: ASDisplayNode {
|
|||||||
|
|
||||||
var filterBecameEmpty: ((ChatListFilter?) -> Void)?
|
var filterBecameEmpty: ((ChatListFilter?) -> Void)?
|
||||||
var filterEmptyAction: ((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
|
self.containerNode = ChatListContainerNode(context: context, location: location, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, presentationData: presentationData, animationCache: animationCache, animationRenderer: animationRenderer, filterBecameEmpty: { filter in
|
||||||
filterBecameEmpty?(filter)
|
filterBecameEmpty?(filter)
|
||||||
}, filterEmptyAction: { filter in
|
}, filterEmptyAction: { filter in
|
||||||
filterEmptyAction?(filter)
|
filterEmptyAction?(filter)
|
||||||
|
}, secondaryEmptyAction: {
|
||||||
|
secondaryEmptyAction?()
|
||||||
})
|
})
|
||||||
|
|
||||||
self.inlineTabContainerNode = ChatListFilterTabInlineContainerNode()
|
self.inlineTabContainerNode = ChatListFilterTabInlineContainerNode()
|
||||||
@ -1156,6 +1182,15 @@ final class ChatListControllerNode: ASDisplayNode {
|
|||||||
strongSelf.emptyListAction?()
|
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
|
self.containerNode.onFilterSwitch = { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.controller?.dismissAllUndoControllers()
|
strongSelf.controller?.dismissAllUndoControllers()
|
||||||
|
@ -11,24 +11,31 @@ import ActivityIndicator
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
final class ChatListEmptyNode: ASDisplayNode {
|
final class ChatListEmptyNode: ASDisplayNode {
|
||||||
|
enum Subject {
|
||||||
|
case chats
|
||||||
|
case filter
|
||||||
|
case forum
|
||||||
|
}
|
||||||
private let action: () -> Void
|
private let action: () -> Void
|
||||||
|
private let secondaryAction: () -> Void
|
||||||
|
|
||||||
let isFilter: Bool
|
let subject: Subject
|
||||||
private(set) var isLoading: Bool
|
private(set) var isLoading: Bool
|
||||||
private let textNode: ImmediateTextNode
|
private let textNode: ImmediateTextNode
|
||||||
private let descriptionNode: ImmediateTextNode
|
private let descriptionNode: ImmediateTextNode
|
||||||
private let animationNode: AnimatedStickerNode
|
private let animationNode: AnimatedStickerNode
|
||||||
private let buttonTextNode: ImmediateTextNode
|
private let buttonNode: SolidRoundedButtonNode
|
||||||
private let buttonNode: HighlightTrackingButtonNode
|
private let secondaryButtonNode: HighlightableButtonNode
|
||||||
private let activityIndicator: ActivityIndicator
|
private let activityIndicator: ActivityIndicator
|
||||||
|
|
||||||
private var animationSize: CGSize = CGSize()
|
private var animationSize: CGSize = CGSize()
|
||||||
|
|
||||||
private var validLayout: 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.action = action
|
||||||
self.isFilter = isFilter
|
self.secondaryAction = secondaryAction
|
||||||
|
self.subject = subject
|
||||||
self.isLoading = isLoading
|
self.isLoading = isLoading
|
||||||
|
|
||||||
self.animationNode = DefaultAnimatedStickerNodeImpl()
|
self.animationNode = DefaultAnimatedStickerNodeImpl()
|
||||||
@ -47,10 +54,9 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
self.descriptionNode.textAlignment = .center
|
self.descriptionNode.textAlignment = .center
|
||||||
self.descriptionNode.lineSpacing = 0.1
|
self.descriptionNode.lineSpacing = 0.1
|
||||||
|
|
||||||
self.buttonNode = HighlightTrackingButtonNode()
|
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), cornerRadius: 11.0, gloss: true)
|
||||||
|
|
||||||
self.buttonTextNode = ImmediateTextNode()
|
self.secondaryButtonNode = HighlightableButtonNode()
|
||||||
self.buttonTextNode.displaysAsynchronously = false
|
|
||||||
|
|
||||||
self.activityIndicator = ActivityIndicator(type: .custom(theme.list.itemAccentColor, 22.0, 1.0, false))
|
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.animationNode)
|
||||||
self.addSubnode(self.textNode)
|
self.addSubnode(self.textNode)
|
||||||
self.addSubnode(self.descriptionNode)
|
self.addSubnode(self.descriptionNode)
|
||||||
self.addSubnode(self.buttonTextNode)
|
|
||||||
self.addSubnode(self.buttonNode)
|
self.addSubnode(self.buttonNode)
|
||||||
|
self.addSubnode(self.secondaryButtonNode)
|
||||||
self.addSubnode(self.activityIndicator)
|
self.addSubnode(self.activityIndicator)
|
||||||
|
|
||||||
let animationName: String
|
let animationName: String
|
||||||
if isFilter {
|
if case .filter = subject {
|
||||||
animationName = "ChatListFilterEmpty"
|
animationName = "ChatListFilterEmpty"
|
||||||
} else {
|
} else {
|
||||||
animationName = "ChatListEmpty"
|
animationName = "ChatListEmpty"
|
||||||
@ -78,23 +84,15 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
self.textNode.isHidden = self.isLoading
|
self.textNode.isHidden = self.isLoading
|
||||||
self.descriptionNode.isHidden = self.isLoading
|
self.descriptionNode.isHidden = self.isLoading
|
||||||
self.buttonNode.isHidden = self.isLoading
|
self.buttonNode.isHidden = self.isLoading
|
||||||
self.buttonTextNode.isHidden = self.isLoading
|
|
||||||
self.activityIndicator.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.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.pressed = { [weak self] in
|
||||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
self?.buttonPressed()
|
||||||
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.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||||
|
|
||||||
self.animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationTapGesture(_:))))
|
self.animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationTapGesture(_:))))
|
||||||
@ -104,6 +102,10 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
self.action()
|
self.action()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func secondaryButtonPressed() {
|
||||||
|
self.secondaryAction()
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func animationTapGesture(_ recognizer: UITapGestureRecognizer) {
|
@objc private func animationTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
if case .ended = recognizer.state {
|
if case .ended = recognizer.state {
|
||||||
if !self.animationNode.isPlaying {
|
if !self.animationNode.isPlaying {
|
||||||
@ -117,18 +119,33 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
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 text: String
|
||||||
let descriptionString: NSAttributedString
|
var descriptionText = ""
|
||||||
if self.isFilter {
|
let buttonText: String
|
||||||
descriptionString = NSAttributedString(string: strings.ChatList_EmptyChatListFilterText, font: Font.regular(14.0), textColor: theme.list.itemSecondaryTextColor)
|
var secondaryButtonText = ""
|
||||||
} else {
|
switch self.subject {
|
||||||
descriptionString = NSAttributedString()
|
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.textNode.attributedText = string
|
||||||
self.descriptionNode.attributedText = descriptionString
|
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)
|
self.activityIndicator.type = .custom(theme.list.itemAccentColor, 22.0, 1.0, false)
|
||||||
|
|
||||||
if let size = self.validLayout {
|
if let size = self.validLayout {
|
||||||
@ -145,7 +162,6 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
self.textNode.isHidden = self.isLoading
|
self.textNode.isHidden = self.isLoading
|
||||||
self.descriptionNode.isHidden = self.isLoading
|
self.descriptionNode.isHidden = self.isLoading
|
||||||
self.buttonNode.isHidden = self.isLoading
|
self.buttonNode.isHidden = self.isLoading
|
||||||
self.buttonTextNode.isHidden = self.isLoading
|
|
||||||
self.activityIndicator.isHidden = !self.isLoading
|
self.activityIndicator.isHidden = !self.isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,16 +173,18 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||||||
|
|
||||||
let animationSpacing: CGFloat = 24.0
|
let animationSpacing: CGFloat = 24.0
|
||||||
let descriptionSpacing: CGFloat = 8.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 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 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 secondaryButtonSize = self.secondaryButtonNode.measure(CGSize(width: buttonWidth, height: .greatestFiniteMagnitude))
|
||||||
let buttonSize = CGSize(width: buttonWidth, height: 50.0)
|
|
||||||
|
|
||||||
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
|
var contentOffset: CGFloat = 0.0
|
||||||
if size.height < contentHeight {
|
if size.height < contentHeight {
|
||||||
contentOffset = -self.animationSize.height - animationSpacing + 44.0
|
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 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 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 descriptionFrame = 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
|
|
||||||
|
|
||||||
if !self.animationSize.width.isZero {
|
if !self.animationSize.width.isZero {
|
||||||
self.animationNode.updateLayout(size: self.animationSize)
|
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.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))
|
var bottomInset: CGFloat = 16.0
|
||||||
let buttonFrame = CGRect(origin: CGPoint(x: floor((size.width - buttonTextSize.width) / 2.0), y: bottomTextEdge + buttonSpacing), size: buttonTextSize)
|
|
||||||
|
|
||||||
|
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.buttonNode, frame: buttonFrame)
|
||||||
transition.updateFrame(node: self.buttonTextNode, frame: buttonFrame)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
if self.buttonNode.frame.contains(point) {
|
if self.buttonNode.frame.contains(point) {
|
||||||
return self.buttonNode.view.hitTest(self.view.convert(point, to: self.buttonNode.view), with: event)
|
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) {
|
if self.animationNode.frame.contains(point) {
|
||||||
return self.animationNode.view.hitTest(self.view.convert(point, to: self.animationNode.view), with: event)
|
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/InviteLinksUI:InviteLinksUI",
|
||||||
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
|
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
|
||||||
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
||||||
|
"//submodules/PersistentStringHash:PersistentStringHash",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//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 updatePublicLinkText: (String?, String) -> Void
|
||||||
let shareLink: () -> 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.account = account
|
||||||
self.updatePublicLinkText = updatePublicLinkText
|
self.updatePublicLinkText = updatePublicLinkText
|
||||||
self.shareLink = shareLink
|
self.shareLink = shareLink
|
||||||
|
self.activateLink = activateLink
|
||||||
|
self.deactivateLink = deactivateLink
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +54,7 @@ private enum UsernameSetupEntry: ItemListNodeEntry {
|
|||||||
case publicLinkInfo(PresentationTheme, String)
|
case publicLinkInfo(PresentationTheme, String)
|
||||||
|
|
||||||
case additionalLinkHeader(PresentationTheme, String)
|
case additionalLinkHeader(PresentationTheme, String)
|
||||||
case additionalLink(PresentationTheme, String, Bool, Int32)
|
case additionalLink(PresentationTheme, TelegramPeerUsername, Int32)
|
||||||
case additionalLinkInfo(PresentationTheme, String)
|
case additionalLinkInfo(PresentationTheme, String)
|
||||||
|
|
||||||
var section: ItemListSectionId {
|
var section: ItemListSectionId {
|
||||||
@ -73,7 +78,7 @@ private enum UsernameSetupEntry: ItemListNodeEntry {
|
|||||||
return 3
|
return 3
|
||||||
case .additionalLinkHeader:
|
case .additionalLinkHeader:
|
||||||
return 4
|
return 4
|
||||||
case let .additionalLink(_, _, _, index):
|
case let .additionalLink(_, _, index):
|
||||||
return 5 + index
|
return 5 + index
|
||||||
case .additionalLinkInfo:
|
case .additionalLinkInfo:
|
||||||
return 1000
|
return 1000
|
||||||
@ -112,8 +117,8 @@ private enum UsernameSetupEntry: ItemListNodeEntry {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .additionalLink(lhsTheme, lhsLink, lhsActive, lhsIndex):
|
case let .additionalLink(lhsTheme, lhsAddressName, lhsIndex):
|
||||||
if case let .additionalLink(rhsTheme, rhsLink, rhsActive, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsLink == rhsLink, lhsActive == rhsActive, lhsIndex == rhsIndex {
|
if case let .additionalLink(rhsTheme, rhsAddressName, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsIndex == rhsIndex {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -167,8 +172,16 @@ private enum UsernameSetupEntry: ItemListNodeEntry {
|
|||||||
return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: string, sectionId: self.section)
|
return ItemListActivityTextItem(displayActivity: displayActivity, presentationData: presentationData, text: string, sectionId: self.section)
|
||||||
case let .additionalLinkHeader(_, text):
|
case let .additionalLinkHeader(_, text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case let .additionalLink(_, link, _, _):
|
case let .additionalLink(_, link, _):
|
||||||
return ItemListTextItem(presentationData: presentationData, text: .plain(link), sectionId: self.section)
|
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):
|
case let .additionalLinkInfo(_, text):
|
||||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
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] = []
|
var entries: [UsernameSetupEntry] = []
|
||||||
|
|
||||||
if let peer = view.peers[view.peerId] as? TelegramUser {
|
if let peer = view.peers[view.peerId] as? TelegramUser {
|
||||||
@ -280,6 +293,27 @@ private func usernameSetupControllerEntries(presentationData: PresentationData,
|
|||||||
|
|
||||||
if !otherUsernames.isEmpty {
|
if !otherUsernames.isEmpty {
|
||||||
entries.append(.additionalLinkHeader(presentationData.theme, presentationData.strings.Username_LinksOrder))
|
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))
|
entries.append(.additionalLinkInfo(presentationData.theme, presentationData.strings.Username_LinksOrderInfo))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -350,76 +384,198 @@ public func usernameSetupController(context: AccountContext) -> ViewController {
|
|||||||
presentControllerImpl?(shareController, nil)
|
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)
|
let peerView = context.account.viewTracker.peerView(context.account.peerId)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|
|
||||||
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get() |> deliverOnMainQueue, peerView)
|
let signal = combineLatest(
|
||||||
|> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
context.sharedContext.presentationData,
|
||||||
let peer = peerViewMainPeer(view)
|
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 addressNameValidationStatus = state.addressNameValidationStatus {
|
||||||
if let peer = peer as? TelegramUser {
|
switch addressNameValidationStatus {
|
||||||
var doneEnabled = true
|
case .availability(.available):
|
||||||
|
break
|
||||||
if let addressNameValidationStatus = state.addressNameValidationStatus {
|
default:
|
||||||
switch addressNameValidationStatus {
|
doneEnabled = false
|
||||||
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: {
|
if let updatedAddressNameValue = updatedAddressNameValue {
|
||||||
var updatedAddressNameValue: String?
|
updateAddressNameDisposable.set((context.engine.peers.updateAddressName(domain: .account, name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue)
|
||||||
updateState { state in
|
|> deliverOnMainQueue).start(error: { _ in
|
||||||
if state.editingPublicLinkText != peer.addressName {
|
updateState { state in
|
||||||
updatedAddressNameValue = state.editingPublicLinkText
|
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?()
|
dismissImpl?()
|
||||||
}
|
}))
|
||||||
})
|
} else {
|
||||||
}
|
dismissImpl?()
|
||||||
|
}
|
||||||
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
|
||||||
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)
|
return (controllerState, (listState, arguments))
|
||||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: usernameSetupControllerEntries(presentationData: presentationData, view: view, state: state), style: .blocks, focusItemTag: UsernameEntryTag.username, animateChanges: false)
|
} |> afterDisposed {
|
||||||
|
actionsDisposable.dispose()
|
||||||
return (controllerState, (listState, arguments))
|
|
||||||
} |> afterDisposed {
|
|
||||||
actionsDisposable.dispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = ItemListController(context: context, state: signal)
|
let controller = ItemListController(context: context, state: signal)
|
||||||
controller.navigationPresentation = .modal
|
controller.navigationPresentation = .modal
|
||||||
controller.enableInteractiveDismiss = true
|
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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
controller.beganInteractiveDragging = {
|
||||||
|
dismissInputImpl?()
|
||||||
|
}
|
||||||
|
|
||||||
dismissImpl = { [weak controller] in
|
dismissImpl = { [weak controller] in
|
||||||
controller?.view.endEditing(true)
|
controller?.view.endEditing(true)
|
||||||
controller?.dismiss()
|
controller?.dismiss()
|
||||||
|
@ -104,11 +104,11 @@ private final class TitleFieldComponent: Component {
|
|||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
let titleCredibilityContent: EmojiStatusComponent.Content
|
let iconContent: EmojiStatusComponent.Content
|
||||||
if component.fileId == 0 {
|
if component.fileId == 0 {
|
||||||
titleCredibilityContent = .topic(title: String(component.text.prefix(1)), colorIndex: 0)
|
iconContent = .topic(title: String(component.text.prefix(1)), colorIndex: 0)
|
||||||
} else {
|
} 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(
|
let iconSize = self.iconView.update(
|
||||||
@ -117,7 +117,7 @@ private final class TitleFieldComponent: Component {
|
|||||||
context: component.context,
|
context: component.context,
|
||||||
animationCache: component.context.animationCache,
|
animationCache: component.context.animationCache,
|
||||||
animationRenderer: component.context.animationRenderer,
|
animationRenderer: component.context.animationRenderer,
|
||||||
content: titleCredibilityContent,
|
content: iconContent,
|
||||||
isVisibleForAnimations: true,
|
isVisibleForAnimations: true,
|
||||||
action: nil
|
action: nil
|
||||||
)),
|
)),
|
||||||
@ -314,12 +314,14 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
|||||||
let peerId: EnginePeer.Id
|
let peerId: EnginePeer.Id
|
||||||
let mode: ForumCreateTopicScreen.Mode
|
let mode: ForumCreateTopicScreen.Mode
|
||||||
let titleUpdated: (String) -> Void
|
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.context = context
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.titleUpdated = titleUpdated
|
self.titleUpdated = titleUpdated
|
||||||
|
self.iconUpdated = iconUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: ForumCreateTopicScreenComponent, rhs: ForumCreateTopicScreenComponent) -> Bool {
|
static func ==(lhs: ForumCreateTopicScreenComponent, rhs: ForumCreateTopicScreenComponent) -> Bool {
|
||||||
@ -338,6 +340,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
|||||||
final class State: ComponentState {
|
final class State: ComponentState {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let titleUpdated: (String) -> Void
|
private let titleUpdated: (String) -> Void
|
||||||
|
private let iconUpdated: (Int64?) -> Void
|
||||||
|
|
||||||
var emojiContent: EmojiPagerContentComponent?
|
var emojiContent: EmojiPagerContentComponent?
|
||||||
private let emojiContentDisposable = MetaDisposable()
|
private let emojiContentDisposable = MetaDisposable()
|
||||||
@ -345,9 +348,10 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
|||||||
var title: String
|
var title: String
|
||||||
var fileId: Int64
|
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.context = context
|
||||||
self.titleUpdated = titleUpdated
|
self.titleUpdated = titleUpdated
|
||||||
|
self.iconUpdated = iconUpdated
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case .create:
|
case .create:
|
||||||
@ -422,6 +426,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
|||||||
self.fileId = item.itemFile?.fileId.id ?? 0
|
self.fileId = item.itemFile?.fileId.id ?? 0
|
||||||
self.updated(transition: .immediate)
|
self.updated(transition: .immediate)
|
||||||
|
|
||||||
|
self.iconUpdated(self.fileId != 0 ? self.fileId : nil)
|
||||||
|
|
||||||
self.emojiContentDisposable.set((
|
self.emojiContentDisposable.set((
|
||||||
EmojiPagerContentComponent.emojiInputData(
|
EmojiPagerContentComponent.emojiInputData(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
@ -449,7 +455,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
|||||||
return State(
|
return State(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
mode: self.mode,
|
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)
|
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) {
|
public init(context: AccountContext, peerId: EnginePeer.Id, mode: ForumCreateTopicScreen.Mode) {
|
||||||
var titleUpdatedImpl: ((String) -> Void)?
|
var titleUpdatedImpl: ((String) -> Void)?
|
||||||
|
var iconUpdatedImpl: ((Int64?) -> Void)?
|
||||||
super.init(context: context, component: ForumCreateTopicScreenComponent(context: context, peerId: peerId, mode: mode, titleUpdated: { title in
|
super.init(context: context, component: ForumCreateTopicScreenComponent(context: context, peerId: peerId, mode: mode, titleUpdated: { title in
|
||||||
titleUpdatedImpl?(title)
|
titleUpdatedImpl?(title)
|
||||||
|
}, iconUpdated: { fileId in
|
||||||
|
iconUpdatedImpl?(fileId)
|
||||||
}), navigationBarAppearance: .transparent)
|
}), navigationBarAppearance: .transparent)
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
@ -692,7 +705,20 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
|
|||||||
self.navigationItem.rightBarButtonItem?.isEnabled = false
|
self.navigationItem.rightBarButtonItem?.isEnabled = false
|
||||||
|
|
||||||
titleUpdatedImpl = { [weak self] title in
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,6 +731,8 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func createPressed() {
|
@objc private func createPressed() {
|
||||||
self.dismiss()
|
// self.dismiss()
|
||||||
|
|
||||||
|
self.completion(self.state.0, self.state.1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -322,6 +322,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
private var editingUrlPreviewQueryState: (String?, Disposable)?
|
private var editingUrlPreviewQueryState: (String?, Disposable)?
|
||||||
private var searchState: ChatSearchState?
|
private var searchState: ChatSearchState?
|
||||||
|
|
||||||
|
private var shakeFeedback: HapticFeedback?
|
||||||
|
|
||||||
private var recordingModeFeedback: HapticFeedback?
|
private var recordingModeFeedback: HapticFeedback?
|
||||||
private var recorderFeedback: HapticFeedback?
|
private var recorderFeedback: HapticFeedback?
|
||||||
private var audioRecorderValue: ManagedAudioRecorder?
|
private var audioRecorderValue: ManagedAudioRecorder?
|
||||||
@ -630,6 +632,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
if strongSelf.presentVoiceMessageDiscardAlert(action: action, performAction: false) {
|
if strongSelf.presentVoiceMessageDiscardAlert(action: action, performAction: false) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strongSelf.presentTopicDiscardAlert(action: action, performAction: false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15671,6 +15678,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
if case let .id(messageId) = messageSubject {
|
if case let .id(messageId) = messageSubject {
|
||||||
strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, timecode))
|
strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, timecode))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self?.playShakeAnimation()
|
||||||
}
|
}
|
||||||
} else if let navigationController = strongSelf.effectiveNavigationController {
|
} else if let navigationController = strongSelf.effectiveNavigationController {
|
||||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peerId.id), subject: subject, keepStack: .always, peekData: peekData))
|
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peerId.id), subject: subject, keepStack: .always, peekData: peekData))
|
||||||
@ -16883,6 +16892,34 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func presentTopicDiscardAlert(action: @escaping () -> Void = {}, delay: Bool = false, performAction: Bool = true) -> Bool {
|
||||||
|
if self.chatDisplayNode.emptyType == .topic {
|
||||||
|
Queue.mainQueue().after(delay ? 0.2 : 0.0) {
|
||||||
|
self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: "Delete Topic", text: "Topic isn't created, because you haven't posted a message.\n\nDo you want to discard this topic?", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Yes, action: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if case let .replyThread(messagePromise) = strongSelf.chatLocationInfoData {
|
||||||
|
let _ = (messagePromise.get()
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] message in
|
||||||
|
if let strongSelf = self, let message = message {
|
||||||
|
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
action()
|
||||||
|
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_No, action: {})]), in: .window(.root))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} else if performAction {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private func presentAutoremoveSetup() {
|
private func presentAutoremoveSetup() {
|
||||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||||
return
|
return
|
||||||
@ -17136,6 +17173,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
return false
|
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 {
|
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||||||
|
@ -93,7 +93,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
let historyNodeContainer: ASDisplayNode
|
let historyNodeContainer: ASDisplayNode
|
||||||
let loadingNode: ChatLoadingNode
|
let loadingNode: ChatLoadingNode
|
||||||
private var emptyNode: ChatEmptyNode?
|
private var emptyNode: ChatEmptyNode?
|
||||||
private var emptyType: ChatHistoryNodeLoadState.EmptyType?
|
private(set) var emptyType: ChatHistoryNodeLoadState.EmptyType?
|
||||||
private var didDisplayEmptyGreeting = false
|
private var didDisplayEmptyGreeting = false
|
||||||
private var validEmptyNodeLayout: (CGSize, UIEdgeInsets)?
|
private var validEmptyNodeLayout: (CGSize, UIEdgeInsets)?
|
||||||
var restrictedNode: ChatRecentActionsEmptyNode?
|
var restrictedNode: ChatRecentActionsEmptyNode?
|
||||||
|
@ -12,6 +12,8 @@ import TelegramStringFormatting
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import ChatPresentationInterfaceState
|
import ChatPresentationInterfaceState
|
||||||
import WallpaperBackgroundNode
|
import WallpaperBackgroundNode
|
||||||
|
import ComponentFlow
|
||||||
|
import EmojiStatusComponent
|
||||||
|
|
||||||
private protocol ChatEmptyNodeContent {
|
private protocol ChatEmptyNodeContent {
|
||||||
func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize
|
func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize
|
||||||
@ -775,13 +777,132 @@ 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 {
|
||||||
|
var colorIndex: Int = 0
|
||||||
|
if case let .replyThread(replyThreadMessage) = interfaceState.chatLocation {
|
||||||
|
colorIndex = Int(clamping: abs(replyThreadMessage.effectiveTopId.id))
|
||||||
|
}
|
||||||
|
iconContent = .topic(title: String(title.prefix(1)), colorIndex: colorIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 regular
|
||||||
case secret
|
case secret
|
||||||
case group
|
case group
|
||||||
case cloud
|
case cloud
|
||||||
case peerNearby
|
case peerNearby
|
||||||
case greeting
|
case greeting
|
||||||
|
case topic(Int64?)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ChatEmptyNode: ASDisplayNode {
|
final class ChatEmptyNode: ASDisplayNode {
|
||||||
@ -857,9 +978,13 @@ final class ChatEmptyNode: ASDisplayNode {
|
|||||||
|
|
||||||
let contentType: ChatEmptyNodeContentType
|
let contentType: ChatEmptyNodeContentType
|
||||||
if case .replyThread = interfaceState.chatLocation {
|
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 {
|
} else if let peer = interfaceState.renderedPeer?.peer, !isScheduledMessages {
|
||||||
if peer.id == self.context.account.peerId {
|
if peer.id == self.context.account.peerId {
|
||||||
contentType = .cloud
|
contentType = .cloud
|
||||||
} else if let _ = peer as? TelegramSecretChat {
|
} else if let _ = peer as? TelegramSecretChat {
|
||||||
contentType = .secret
|
contentType = .secret
|
||||||
@ -890,7 +1015,7 @@ final class ChatEmptyNode: ASDisplayNode {
|
|||||||
var animateContentIn = false
|
var animateContentIn = false
|
||||||
if let node = self.content?.1 {
|
if let node = self.content?.1 {
|
||||||
node.removeFromSupernode()
|
node.removeFromSupernode()
|
||||||
if self.content?.0 != nil && contentType == .greeting && transition.isAnimated {
|
if self.content?.0 != nil, case .greeting = contentType, transition.isAnimated {
|
||||||
animateContentIn = true
|
animateContentIn = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -909,6 +1034,8 @@ final class ChatEmptyNode: ASDisplayNode {
|
|||||||
case .greeting:
|
case .greeting:
|
||||||
node = ChatEmptyNodeGreetingChatContent(context: self.context, interaction: self.interaction)
|
node = ChatEmptyNodeGreetingChatContent(context: self.context, interaction: self.interaction)
|
||||||
updateGreetingSticker = true
|
updateGreetingSticker = true
|
||||||
|
case .topic:
|
||||||
|
node = ChatEmptyNodeTopicChatContent(context: self.context, fileId: nil)
|
||||||
}
|
}
|
||||||
self.content = (contentType, node)
|
self.content = (contentType, node)
|
||||||
self.addSubnode(node)
|
self.addSubnode(node)
|
||||||
|
@ -75,7 +75,7 @@ func chatHistoryEntriesForView(
|
|||||||
associatedMedia: [:]
|
associatedMedia: [:]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = []
|
var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = []
|
||||||
var count = 0
|
var count = 0
|
||||||
loop: for entry in view.entries {
|
loop: for entry in view.entries {
|
||||||
@ -211,6 +211,19 @@ func chatHistoryEntriesForView(
|
|||||||
|
|
||||||
let topMessage = messages[0]
|
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?
|
var adminRank: CachedChannelAdminRank?
|
||||||
if let author = topMessage.author {
|
if let author = topMessage.author {
|
||||||
adminRank = adminRanks[author.id]
|
adminRank = adminRanks[author.id]
|
||||||
@ -235,12 +248,13 @@ func chatHistoryEntriesForView(
|
|||||||
}
|
}
|
||||||
entries.insert(.MessageGroupEntry(groupInfo, groupMessages, presentationData), at: 0)
|
entries.insert(.MessageGroupEntry(groupInfo, groupMessages, presentationData), at: 0)
|
||||||
} else {
|
} 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !replyThreadMessage.isForumPost {
|
if !replyThreadMessage.isForumPost {
|
||||||
let replyCount = view.entries.isEmpty ? 0 : 1
|
let replyCount = view.entries.isEmpty ? 0 : 1
|
||||||
|
|
||||||
entries.insert(.ReplyCountEntry(messages[0].index, replyThreadMessage.isChannelPost, replyCount, presentationData), at: 1)
|
entries.insert(.ReplyCountEntry(messages[0].index, replyThreadMessage.isChannelPost, replyCount, presentationData), at: 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2527,12 +2527,36 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
} else if action.action == .historyCleared {
|
} else if action.action == .historyCleared {
|
||||||
emptyType = .clearedHistory
|
emptyType = .clearedHistory
|
||||||
break
|
break
|
||||||
|
} else if case .topicCreated = action.action {
|
||||||
|
emptyType = .topic
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadState = .empty(emptyType)
|
loadState = .empty(emptyType)
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
loadState = .messages
|
loadState = .messages
|
||||||
|
@ -11,6 +11,7 @@ public enum ChatHistoryNodeLoadState: Equatable {
|
|||||||
case generic
|
case generic
|
||||||
case joined
|
case joined
|
||||||
case clearedHistory
|
case clearedHistory
|
||||||
|
case topic
|
||||||
}
|
}
|
||||||
|
|
||||||
case loading
|
case loading
|
||||||
|
@ -13,6 +13,7 @@ import SettingsUI
|
|||||||
import ChatPresentationInterfaceState
|
import ChatPresentationInterfaceState
|
||||||
import AttachmentUI
|
import AttachmentUI
|
||||||
import ForumTopicListScreen
|
import ForumTopicListScreen
|
||||||
|
import ForumCreateTopicScreen
|
||||||
|
|
||||||
public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) {
|
public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) {
|
||||||
var found = false
|
var found = false
|
||||||
@ -110,6 +111,9 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let viewControllers = params.navigationController.viewControllers.filter({ controller in
|
let viewControllers = params.navigationController.viewControllers.filter({ controller in
|
||||||
|
if controller is ForumCreateTopicScreen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if controller is ChatListController {
|
if controller is ChatListController {
|
||||||
if let parentGroupId = params.parentGroupId {
|
if let parentGroupId = params.parentGroupId {
|
||||||
return parentGroupId != .root
|
return parentGroupId != .root
|
||||||
@ -224,7 +228,7 @@ public func isOverlayControllerForChatNotificationOverlayPresentation(_ controll
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, navigationController: NavigationController) -> Signal<Never, NoError> {
|
public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, navigationController: NavigationController, activateInput: ChatControllerActivateInput?) -> Signal<Never, NoError> {
|
||||||
return fetchAndPreloadReplyThreadInfo(context: context, subject: .groupMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))), atMessageId: nil, preload: false)
|
return fetchAndPreloadReplyThreadInfo(context: context, subject: .groupMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))), atMessageId: nil, preload: false)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> beforeNext { [weak context, weak navigationController] result in
|
|> beforeNext { [weak context, weak navigationController] result in
|
||||||
@ -237,7 +241,12 @@ public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePee
|
|||||||
let subject: ChatControllerSubject?
|
let subject: ChatControllerSubject?
|
||||||
subject = nil
|
subject = nil
|
||||||
|
|
||||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: chatLocation, chatLocationContextHolder: result.contextHolder, subject: subject, activateInput: result.isEmpty ? .text : nil, keepStack: .always))
|
var actualActivateInput: ChatControllerActivateInput? = result.isEmpty ? .text : nil
|
||||||
|
if let activateInput = activateInput {
|
||||||
|
actualActivateInput = activateInput
|
||||||
|
}
|
||||||
|
|
||||||
|
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: chatLocation, chatLocationContextHolder: result.contextHolder, subject: subject, activateInput: actualActivateInput, keepStack: .never))
|
||||||
}
|
}
|
||||||
|> ignoreValues
|
|> ignoreValues
|
||||||
|> `catch` { _ -> Signal<Never, NoError> in
|
|> `catch` { _ -> Signal<Never, NoError> in
|
||||||
|
@ -2570,8 +2570,9 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
if self.isSettings, let user = peer as? TelegramUser {
|
if self.isSettings, let user = peer as? TelegramUser {
|
||||||
var subtitle = formatPhoneNumber(user.phone ?? "")
|
var subtitle = formatPhoneNumber(user.phone ?? "")
|
||||||
|
|
||||||
if let addressName = user.addressName, !addressName.isEmpty {
|
let mainUsername = user.usernames.first?.username ?? user.username
|
||||||
subtitle = "\(subtitle) • @\(addressName)"
|
if let mainUsername = mainUsername, !mainUsername.isEmpty {
|
||||||
|
subtitle = "\(subtitle) • @\(mainUsername)"
|
||||||
}
|
}
|
||||||
smallSubtitleString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7))
|
smallSubtitleString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.7))
|
||||||
subtitleString = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
subtitleString = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: presentationData.theme.list.itemSecondaryTextColor)
|
||||||
|
@ -955,8 +955,9 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if let username = user.username {
|
if let username = user.username {
|
||||||
|
let mainUsername = user.usernames.first?.username ?? username
|
||||||
var additionalUsernames: String?
|
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 {
|
if !usernames.isEmpty {
|
||||||
additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string
|
additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string
|
||||||
}
|
}
|
||||||
@ -965,7 +966,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
PeerInfoScreenLabeledValueItem(
|
PeerInfoScreenLabeledValueItem(
|
||||||
id: 1,
|
id: 1,
|
||||||
label: presentationData.strings.Profile_Username,
|
label: presentationData.strings.Profile_Username,
|
||||||
text: "@\(username)",
|
text: "@\(mainUsername)",
|
||||||
additionalText: additionalUsernames,
|
additionalText: additionalUsernames,
|
||||||
textColor: .accent,
|
textColor: .accent,
|
||||||
icon: .qrCode,
|
icon: .qrCode,
|
||||||
@ -973,6 +974,12 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
interaction.openUsername(username)
|
interaction.openUsername(username)
|
||||||
}, longTapAction: { sourceNode in
|
}, longTapAction: { sourceNode in
|
||||||
interaction.openPeerInfoContextMenu(.link, sourceNode)
|
interaction.openPeerInfoContextMenu(.link, sourceNode)
|
||||||
|
}, linkItemAction: { type, item in
|
||||||
|
if case .tap = type {
|
||||||
|
if case let .mention(username) = item {
|
||||||
|
interaction.openUsername(String(username[username.index(username.startIndex, offsetBy: 1)...]))
|
||||||
|
}
|
||||||
|
}
|
||||||
}, iconAction: {
|
}, iconAction: {
|
||||||
interaction.openQrCode()
|
interaction.openQrCode()
|
||||||
}, requestLayout: {
|
}, requestLayout: {
|
||||||
@ -1087,17 +1094,18 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
|
|
||||||
if let username = channel.username {
|
if let username = channel.username {
|
||||||
var additionalUsernames: String?
|
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 {
|
if !usernames.isEmpty {
|
||||||
additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string
|
additionalUsernames = presentationData.strings.Profile_AdditionalUsernames(String(usernames.map { "@\($0.username)" }.joined(separator: ", "))).string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
items[.peerInfo]!.append(
|
items[.peerInfo]!.append(
|
||||||
PeerInfoScreenLabeledValueItem(
|
PeerInfoScreenLabeledValueItem(
|
||||||
id: ItemUsername,
|
id: ItemUsername,
|
||||||
label: presentationData.strings.Channel_LinkItem,
|
label: presentationData.strings.Channel_LinkItem,
|
||||||
text: "https://t.me/\(username)",
|
text: "https://t.me/\(mainUsername)",
|
||||||
additionalText: additionalUsernames,
|
additionalText: additionalUsernames,
|
||||||
textColor: .accent,
|
textColor: .accent,
|
||||||
icon: .qrCode,
|
icon: .qrCode,
|
||||||
@ -1105,6 +1113,12 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
|||||||
interaction.openUsername(username)
|
interaction.openUsername(username)
|
||||||
}, longTapAction: { sourceNode in
|
}, longTapAction: { sourceNode in
|
||||||
interaction.openPeerInfoContextMenu(.link, sourceNode)
|
interaction.openPeerInfoContextMenu(.link, sourceNode)
|
||||||
|
}, linkItemAction: { type, item in
|
||||||
|
if case .tap = type {
|
||||||
|
if case let .mention(username) = item {
|
||||||
|
interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1))))
|
||||||
|
}
|
||||||
|
}
|
||||||
}, iconAction: {
|
}, iconAction: {
|
||||||
interaction.openQrCode()
|
interaction.openQrCode()
|
||||||
}, requestLayout: {
|
}, requestLayout: {
|
||||||
|
@ -1159,8 +1159,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
navigateToForumChannelImpl(context: context, peerId: peerId, navigationController: navigationController)
|
navigateToForumChannelImpl(context: context, peerId: peerId, navigationController: navigationController)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, navigationController: NavigationController) -> Signal<Never, NoError> {
|
public func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, navigationController: NavigationController, activateInput: ChatControllerActivateInput?) -> Signal<Never, NoError> {
|
||||||
return navigateToForumThreadImpl(context: context, peerId: peerId, threadId: threadId, navigationController: navigationController)
|
return navigateToForumThreadImpl(context: context, peerId: peerId, threadId: threadId, navigationController: navigationController, activateInput: activateInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func openStorageUsage(context: AccountContext) {
|
public func openStorageUsage(context: AccountContext) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user