mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Business features
This commit is contained in:
parent
6b227b8aa4
commit
5fc369ae4a
@ -995,6 +995,7 @@ public protocol ChatController: ViewController {
|
||||
var isSelectingMessagesUpdated: ((Bool) -> Void)? { get set }
|
||||
func cancelSelectingMessages()
|
||||
func activateSearch(domain: ChatSearchDomain, query: String)
|
||||
func activateInput(type: ChatControllerActivateInput)
|
||||
func beginClearHistory(type: InteractiveHistoryClearingType)
|
||||
|
||||
func performScrollToTop() -> Bool
|
||||
|
@ -812,7 +812,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
}
|
||||
}
|
||||
|
||||
let openNewCard: (String?) -> Void = { [weak self] customUrl in
|
||||
let openNewCard: (String?, String?) -> Void = { [weak self] customUrl, customTitle in
|
||||
if let strongSelf = self, let paymentForm = strongSelf.paymentFormValue {
|
||||
if customUrl == nil, let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "stripe" {
|
||||
guard let paramsData = nativeProvider.params.data(using: .utf8) else {
|
||||
@ -970,7 +970,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
strongSelf.present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
} else {
|
||||
var dismissImpl: (() -> Void)?
|
||||
let controller = BotCheckoutWebInteractionController(context: context, url: customUrl ?? paymentForm.url, intent: .addPaymentMethod({ [weak self] token in
|
||||
let controller = BotCheckoutWebInteractionController(context: context, url: customUrl ?? paymentForm.url, intent: .addPaymentMethod(customTitle: customTitle, completion: { [weak self] token in
|
||||
dismissImpl?()
|
||||
|
||||
guard let strongSelf = self else {
|
||||
@ -1069,14 +1069,14 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
strongSelf.controller?.view.endEditing(true)
|
||||
let methods = availablePaymentMethods(form: paymentForm, current: strongSelf.currentPaymentMethod)
|
||||
if methods.isEmpty {
|
||||
openNewCard(nil)
|
||||
openNewCard(nil, nil)
|
||||
} else {
|
||||
strongSelf.present(BotCheckoutPaymentMethodSheetController(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in
|
||||
applyPaymentMethod(method)
|
||||
}, newCard: {
|
||||
openNewCard(nil)
|
||||
}, otherMethod: { url in
|
||||
openNewCard(url)
|
||||
openNewCard(nil, nil)
|
||||
}, otherMethod: { url, title in
|
||||
openNewCard(url, title)
|
||||
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ enum BotCheckoutPaymentMethod: Equatable {
|
||||
final class BotCheckoutPaymentMethodSheetController: ActionSheetController {
|
||||
private var presentationDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String) -> Void) {
|
||||
init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
@ -83,7 +83,7 @@ final class BotCheckoutPaymentMethodSheetController: ActionSheetController {
|
||||
}
|
||||
items.append(BotCheckoutPaymentMethodItem(title: title, icon: icon, value: value, action: { [weak self] _ in
|
||||
if case let .other(method) = method {
|
||||
otherMethod(method.url)
|
||||
otherMethod(method.url, method.title)
|
||||
} else {
|
||||
applyValue(method)
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import TelegramPresentationData
|
||||
import AccountContext
|
||||
|
||||
enum BotCheckoutWebInteractionControllerIntent {
|
||||
case addPaymentMethod((BotCheckoutPaymentWebToken) -> Void)
|
||||
case addPaymentMethod(customTitle: String?, completion: (BotCheckoutPaymentWebToken) -> Void)
|
||||
case externalVerification((Bool) -> Void)
|
||||
}
|
||||
|
||||
@ -39,10 +39,10 @@ final class BotCheckoutWebInteractionController: ViewController {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
|
||||
switch intent {
|
||||
case .addPaymentMethod:
|
||||
self.title = self.presentationData.strings.Checkout_NewCard_Title
|
||||
case .externalVerification:
|
||||
self.title = self.presentationData.strings.Checkout_WebConfirmation_Title
|
||||
case let .addPaymentMethod(customTitle, _):
|
||||
self.title = customTitle ?? self.presentationData.strings.Checkout_NewCard_Title
|
||||
case .externalVerification:
|
||||
self.title = self.presentationData.strings.Checkout_WebConfirmation_Title
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,7 @@ final class BotCheckoutWebInteractionControllerNode: ViewControllerTracingNode,
|
||||
return
|
||||
}
|
||||
|
||||
if case let .addPaymentMethod(completion) = self.intent {
|
||||
if case let .addPaymentMethod(_, completion) = self.intent {
|
||||
completion(BotCheckoutPaymentWebToken(title: title, data: credentialsString, saveOnServer: false))
|
||||
}
|
||||
}
|
||||
|
@ -910,7 +910,7 @@ private final class ChatListMediaPreviewNode: ASDisplayNode {
|
||||
|
||||
private let loginCodeRegex = try? NSRegularExpression(pattern: "[\\d\\-]{5,7}", options: [])
|
||||
|
||||
class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
final class TopicItemNode: ASDisplayNode {
|
||||
let topicTitleNode: TextNode
|
||||
let titleTopicIconView: ComponentHostView<Empty>
|
||||
@ -998,13 +998,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
final class AuthorNode: ASDisplayNode {
|
||||
let authorNode: TextNode
|
||||
public final class AuthorNode: ASDisplayNode {
|
||||
public let authorNode: TextNode
|
||||
var titleTopicArrowNode: ASImageNode?
|
||||
var topicNodes: [Int64: TopicItemNode] = [:]
|
||||
var topicNodeOrder: [Int64] = []
|
||||
|
||||
var visibilityStatus: Bool = false {
|
||||
public var visibilityStatus: Bool = false {
|
||||
didSet {
|
||||
if self.visibilityStatus != oldValue {
|
||||
for (_, topicNode) in self.topicNodes {
|
||||
@ -1014,7 +1014,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
override public init() {
|
||||
self.authorNode = TextNode()
|
||||
self.authorNode.displaysAsynchronously = true
|
||||
|
||||
@ -1186,8 +1186,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
let contextContainer: ContextControllerSourceNode
|
||||
let mainContentContainerNode: ASDisplayNode
|
||||
|
||||
let avatarContainerNode: ASDisplayNode
|
||||
let avatarNode: AvatarNode
|
||||
public let avatarContainerNode: ASDisplayNode
|
||||
public let avatarNode: AvatarNode
|
||||
var avatarIconView: ComponentHostView<Empty>?
|
||||
var avatarIconComponent: EmojiStatusComponent?
|
||||
var avatarVideoNode: AvatarVideoNode?
|
||||
@ -1196,23 +1196,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
private var inlineNavigationMarkLayer: SimpleLayer?
|
||||
|
||||
let titleNode: TextNode
|
||||
let authorNode: AuthorNode
|
||||
public let titleNode: TextNode
|
||||
public let authorNode: AuthorNode
|
||||
private var compoundHighlightingNode: LinkHighlightingNode?
|
||||
private var textArrowNode: ASImageNode?
|
||||
private var compoundTextButtonNode: HighlightTrackingButtonNode?
|
||||
let measureNode: TextNode
|
||||
private var currentItemHeight: CGFloat?
|
||||
let forwardedIconNode: ASImageNode
|
||||
let textNode: TextNodeWithEntities
|
||||
public let textNode: TextNodeWithEntities
|
||||
var trailingTextBadgeNode: TextNode?
|
||||
var trailingTextBadgeBackground: UIImageView?
|
||||
var dustNode: InvisibleInkDustNode?
|
||||
let inputActivitiesNode: ChatListInputActivitiesNode
|
||||
let dateNode: TextNode
|
||||
public let dateNode: TextNode
|
||||
var dateStatusIconNode: ASImageNode?
|
||||
var dateDisclosureIconView: UIImageView?
|
||||
let separatorNode: ASDisplayNode
|
||||
public let separatorNode: ASDisplayNode
|
||||
let statusNode: ChatListStatusNode
|
||||
let badgeNode: ChatListBadgeNode
|
||||
let mentionBadgeNode: ChatListBadgeNode
|
||||
@ -1255,7 +1255,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
private var onlineIsVoiceChat: Bool = false
|
||||
private var currentOnline: Bool?
|
||||
|
||||
override var canBeSelected: Bool {
|
||||
override public var canBeSelected: Bool {
|
||||
if self.selectableControlNode != nil || self.item?.editing == true {
|
||||
return false
|
||||
} else {
|
||||
@ -1263,26 +1263,26 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override var defaultAccessibilityLabel: String? {
|
||||
override public var defaultAccessibilityLabel: String? {
|
||||
get {
|
||||
return self.accessibilityLabel
|
||||
} set(value) {
|
||||
}
|
||||
}
|
||||
override var accessibilityAttributedLabel: NSAttributedString? {
|
||||
override public var accessibilityAttributedLabel: NSAttributedString? {
|
||||
get {
|
||||
return self.accessibilityLabel.flatMap(NSAttributedString.init(string:))
|
||||
} set(value) {
|
||||
}
|
||||
}
|
||||
override var accessibilityAttributedValue: NSAttributedString? {
|
||||
override public var accessibilityAttributedValue: NSAttributedString? {
|
||||
get {
|
||||
return self.accessibilityValue.flatMap(NSAttributedString.init(string:))
|
||||
} set(value) {
|
||||
}
|
||||
}
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
override public var accessibilityLabel: String? {
|
||||
get {
|
||||
guard let item = self.item else {
|
||||
return nil
|
||||
@ -1314,7 +1314,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override var accessibilityValue: String? {
|
||||
override public var accessibilityValue: String? {
|
||||
get {
|
||||
guard let item = self.item else {
|
||||
return nil
|
||||
@ -1382,7 +1382,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
override public var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
let wasVisible = self.visibilityStatus
|
||||
let isVisible: Bool
|
||||
@ -1575,7 +1575,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
threadId = topicItem.id
|
||||
}
|
||||
}
|
||||
item.interaction.activateChatPreview(item, threadId, strongSelf.contextContainer, gesture, nil)
|
||||
item.interaction.activateChatPreview?(item, threadId, strongSelf.contextContainer, gesture, nil)
|
||||
}
|
||||
|
||||
self.onDidLoad { [weak self] _ in
|
||||
@ -1592,11 +1592,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
self.cachedDataDisposable.dispose()
|
||||
}
|
||||
|
||||
override func secondaryAction(at point: CGPoint) {
|
||||
override public func secondaryAction(at point: CGPoint) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
item.interaction.activateChatPreview(item, nil, self.contextContainer, nil, point)
|
||||
item.interaction.activateChatPreview?(item, nil, self.contextContainer, nil, point)
|
||||
}
|
||||
|
||||
func setupItem(item: ChatListItem, synchronousLoads: Bool) {
|
||||
@ -1632,6 +1632,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: .archivedChatsIcon(hiddenByDefault: groupReferenceData.hiddenByDefault), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
|
||||
}
|
||||
|
||||
if item.interaction.activateChatPreview == nil {
|
||||
enablePreview = false
|
||||
}
|
||||
|
||||
self.avatarNode.setStoryStats(storyStats: storyState.flatMap { storyState in
|
||||
return AvatarNode.StoryStats(
|
||||
totalCount: storyState.stats.totalCount,
|
||||
@ -1768,7 +1772,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
self.contextContainer.isGestureEnabled = enablePreview && !item.editing
|
||||
}
|
||||
|
||||
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
let layout = self.asyncLayout()
|
||||
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem)
|
||||
let (nodeLayout, apply) = layout(item as! ChatListItem, params, first, last, firstWithHeader, nextIsPinned)
|
||||
@ -1781,7 +1785,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
return UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
self.isHighlighted = highlighted
|
||||
@ -1846,7 +1850,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func tapped() {
|
||||
override public func tapped() {
|
||||
guard let item = self.item, item.editing else {
|
||||
return
|
||||
}
|
||||
@ -4414,11 +4418,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
item.interaction.openForumThread(index.messageIndex.id.peerId, topicItem.id)
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.clipsToBounds = true
|
||||
if self.skipFadeout {
|
||||
self.skipFadeout = false
|
||||
@ -4445,20 +4449,20 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
transition.updateBounds(node: self.contextContainer, bounds: self.contextContainer.frame.offsetBy(dx: -offset, dy: 0.0))
|
||||
}
|
||||
|
||||
override func touchesToOtherItemsPrevented() {
|
||||
override public func touchesToOtherItemsPrevented() {
|
||||
super.touchesToOtherItemsPrevented()
|
||||
if let item = self.item {
|
||||
item.interaction.setPeerIdWithRevealedOptions(nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyOpened() {
|
||||
override public func revealOptionsInteractivelyOpened() {
|
||||
if let item = self.item {
|
||||
switch item.index {
|
||||
case let .chatList(index):
|
||||
@ -4469,7 +4473,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionsInteractivelyClosed() {
|
||||
override public func revealOptionsInteractivelyClosed() {
|
||||
if let item = self.item {
|
||||
switch item.index {
|
||||
case let .chatList(index):
|
||||
@ -4480,7 +4484,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
||||
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
@ -4607,7 +4611,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func isReorderable(at point: CGPoint) -> Bool {
|
||||
override public func isReorderable(at point: CGPoint) -> Bool {
|
||||
if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) {
|
||||
return true
|
||||
}
|
||||
@ -4632,7 +4636,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
self.avatarNode.playArchiveAnimation()
|
||||
}
|
||||
|
||||
override func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
|
||||
override public func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
|
||||
super.animateFrameTransition(progress, currentValue)
|
||||
|
||||
if let item = self.item {
|
||||
@ -4652,14 +4656,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func snapshotForReordering() -> UIView? {
|
||||
override public func snapshotForReordering() -> UIView? {
|
||||
self.backgroundNode.alpha = 0.9
|
||||
let result = self.view.snapshotContentTree()
|
||||
self.backgroundNode.alpha = 1.0
|
||||
return result
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard let item = self.item else {
|
||||
return nil
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ public final class ChatListNodeInteraction {
|
||||
let toggleArchivedFolderHiddenByDefault: () -> Void
|
||||
let toggleThreadsSelection: ([Int64], Bool) -> Void
|
||||
let hidePsa: (EnginePeer.Id) -> Void
|
||||
let activateChatPreview: (ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void
|
||||
let activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
||||
let present: (ViewController) -> Void
|
||||
let openForumThread: (EnginePeer.Id, Int64) -> Void
|
||||
let openStorageManagement: () -> Void
|
||||
@ -149,7 +149,7 @@ public final class ChatListNodeInteraction {
|
||||
toggleArchivedFolderHiddenByDefault: @escaping () -> Void,
|
||||
toggleThreadsSelection: @escaping ([Int64], Bool) -> Void,
|
||||
hidePsa: @escaping (EnginePeer.Id) -> Void,
|
||||
activateChatPreview: @escaping (ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void,
|
||||
activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?,
|
||||
present: @escaping (ViewController) -> Void,
|
||||
openForumThread: @escaping (EnginePeer.Id, Int64) -> Void,
|
||||
openStorageManagement: @escaping () -> Void,
|
||||
|
@ -387,7 +387,7 @@ private let avatarFont = avatarPlaceholderFont(size: 16.0)
|
||||
public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topSeparatorNode: ASDisplayNode
|
||||
private let separatorNode: ASDisplayNode
|
||||
public let separatorNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
@ -406,12 +406,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
private var avatarBadge: UIImageView?
|
||||
private var avatarIconView: ComponentHostView<Empty>?
|
||||
private var avatarIconComponent: EmojiStatusComponent?
|
||||
private let titleNode: TextNode
|
||||
public let titleNode: TextNode
|
||||
private var credibilityIconView: ComponentHostView<Empty>?
|
||||
private var credibilityIconComponent: EmojiStatusComponent?
|
||||
private var verifiedIconView: ComponentHostView<Empty>?
|
||||
private var verifiedIconComponent: EmojiStatusComponent?
|
||||
private let statusNode: TextNode
|
||||
public let statusNode: TextNode
|
||||
private var statusIconNode: ASImageNode?
|
||||
private var badgeBackgroundNode: ASImageNode?
|
||||
private var badgeTextNode: TextNode?
|
||||
|
@ -393,6 +393,7 @@ final class MutableMessageHistoryView: MutablePostboxView {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
self.sampledState = self.state.sample(postbox: postbox, clipHoles: self.clipHoles)
|
||||
|
||||
self.render(postbox: postbox)
|
||||
|
@ -417,7 +417,9 @@ open class TabBarControllerImpl: ViewController, TabBarController {
|
||||
} else {
|
||||
tabBarHeight = 49.0 + bottomInset
|
||||
}
|
||||
updatedLayout.intrinsicInsets.bottom = tabBarHeight
|
||||
if !self.tabBarControllerNode.tabBarHidden {
|
||||
updatedLayout.intrinsicInsets.bottom = tabBarHeight
|
||||
}
|
||||
|
||||
currentController.containerLayoutUpdated(updatedLayout, transition: transition)
|
||||
}
|
||||
|
@ -96,6 +96,9 @@ extension TelegramUser {
|
||||
if (flags2 & (1 << 1)) != 0 {
|
||||
botFlags.insert(.canEdit)
|
||||
}
|
||||
if (flags2 & (1 << 11)) != 0 {
|
||||
botFlags.insert(.isBusiness)
|
||||
}
|
||||
botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder)
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ public struct BotUserInfoFlags: OptionSet {
|
||||
public static let requiresGeolocationForInlineRequests = BotUserInfoFlags(rawValue: (1 << 3))
|
||||
public static let canBeAddedToAttachMenu = BotUserInfoFlags(rawValue: (1 << 4))
|
||||
public static let canEdit = BotUserInfoFlags(rawValue: (1 << 5))
|
||||
public static let isBusiness = BotUserInfoFlags(rawValue: (1 << 5))
|
||||
}
|
||||
|
||||
public struct BotUserInfo: PostboxCoding, Equatable {
|
||||
|
@ -10,25 +10,27 @@ import ContextUI
|
||||
import TelegramCore
|
||||
import ChatListUI
|
||||
import Postbox
|
||||
import StoryContainerScreen
|
||||
import AvatarNode
|
||||
|
||||
final class PeerInfoScreenPersonalChannelItem: PeerInfoScreenItem {
|
||||
let id: AnyHashable
|
||||
let context: AccountContext
|
||||
let data: PeerInfoPersonalChannelData
|
||||
let requestLayout: (Bool) -> Void
|
||||
let controller: () -> ViewController?
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
id: AnyHashable,
|
||||
context: AccountContext,
|
||||
data: PeerInfoPersonalChannelData,
|
||||
requestLayout: @escaping (Bool) -> Void,
|
||||
controller: @escaping () -> ViewController?,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.id = id
|
||||
self.context = context
|
||||
self.data = data
|
||||
self.requestLayout = requestLayout
|
||||
self.controller = controller
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@ -37,6 +39,261 @@ final class PeerInfoScreenPersonalChannelItem: PeerInfoScreenItem {
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoadingOverlayShimmerNode: ASDisplayNode {
|
||||
private var currentBackgroundColor: UIColor?
|
||||
private var currentForegroundColor: UIColor?
|
||||
private let imageNodeContainer: ASDisplayNode
|
||||
private let imageNode: ASImageNode
|
||||
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
private var isCurrentlyInHierarchy = false
|
||||
private var shouldBeAnimating = false
|
||||
|
||||
override init() {
|
||||
self.imageNodeContainer = ASDisplayNode()
|
||||
self.imageNodeContainer.isLayerBacked = true
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.displayWithoutProcessing = true
|
||||
self.imageNode.contentMode = .scaleToFill
|
||||
|
||||
super.init()
|
||||
|
||||
self.imageNodeContainer.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.imageNodeContainer)
|
||||
}
|
||||
|
||||
override func didEnterHierarchy() {
|
||||
super.didEnterHierarchy()
|
||||
|
||||
self.isCurrentlyInHierarchy = true
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
override func didExitHierarchy() {
|
||||
super.didExitHierarchy()
|
||||
|
||||
self.isCurrentlyInHierarchy = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
|
||||
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
|
||||
return
|
||||
}
|
||||
self.currentBackgroundColor = backgroundColor
|
||||
self.currentForegroundColor = foregroundColor
|
||||
self.backgroundColor = backgroundColor
|
||||
|
||||
self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.clip(to: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let transparentColor = backgroundColor.cgColor
|
||||
let peakColor = foregroundColor.cgColor
|
||||
|
||||
var locations: [CGFloat] = [0.0, 0.5, 1.0]
|
||||
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
|
||||
return
|
||||
}
|
||||
let sizeUpdated = self.absoluteLocation?.1 != containerSize
|
||||
let frameUpdated = self.absoluteLocation?.0 != rect
|
||||
self.absoluteLocation = (rect, containerSize)
|
||||
|
||||
if sizeUpdated {
|
||||
if self.shouldBeAnimating {
|
||||
self.imageNode.layer.removeAnimation(forKey: "shimmer")
|
||||
self.addImageAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
if frameUpdated {
|
||||
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
|
||||
}
|
||||
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
private func updateAnimation() {
|
||||
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
|
||||
if shouldBeAnimating != self.shouldBeAnimating {
|
||||
self.shouldBeAnimating = shouldBeAnimating
|
||||
if shouldBeAnimating {
|
||||
self.addImageAnimation()
|
||||
} else {
|
||||
self.imageNode.layer.removeAnimation(forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addImageAnimation() {
|
||||
guard let containerSize = self.absoluteLocation?.1 else {
|
||||
return
|
||||
}
|
||||
let gradientHeight: CGFloat = 250.0
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
|
||||
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
|
||||
animation.repeatCount = Float.infinity
|
||||
animation.beginTime = 1.0
|
||||
self.imageNode.layer.add(animation, forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
|
||||
public final class LoadingOverlayNode: ASDisplayNode {
|
||||
private let effectNode: LoadingOverlayShimmerNode
|
||||
private let maskNode: ASImageNode
|
||||
private var currentParams: (size: CGSize, presentationData: PresentationData)?
|
||||
|
||||
override public init() {
|
||||
self.effectNode = LoadingOverlayShimmerNode()
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.effectNode)
|
||||
self.effectNode.view.mask = self.maskNode.view
|
||||
}
|
||||
|
||||
public func update(context: AccountContext, size: CGSize, isInlineMode: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
|
||||
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData {
|
||||
self.currentParams = (size, presentationData)
|
||||
|
||||
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
|
||||
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil))
|
||||
let timestamp1: Int32 = 100000
|
||||
let peers: [EnginePeer.Id: EnginePeer] = [:]
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
})
|
||||
|
||||
let items = (0 ..< 1).map { _ -> ChatListItem in
|
||||
let message = EngineMessage(
|
||||
stableId: 0,
|
||||
stableVersion: 0,
|
||||
id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0),
|
||||
globallyUniqueId: nil,
|
||||
groupingKey: nil,
|
||||
groupInfo: nil,
|
||||
threadId: nil,
|
||||
timestamp: timestamp1,
|
||||
flags: [],
|
||||
tags: [],
|
||||
globalTags: [],
|
||||
localTags: [],
|
||||
customTags: [],
|
||||
forwardInfo: nil,
|
||||
author: peer1,
|
||||
text: "Text",
|
||||
attributes: [],
|
||||
media: [],
|
||||
peers: peers,
|
||||
associatedMessages: [:],
|
||||
associatedMessageIds: [],
|
||||
associatedMedia: [:],
|
||||
associatedThreadInfo: nil,
|
||||
associatedStories: [:]
|
||||
)
|
||||
let readState = EnginePeerReadCounters()
|
||||
|
||||
return ChatListItem(presentationData: chatListPresentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1))), content: .peer(ChatListItemContent.PeerData(
|
||||
messages: [message],
|
||||
peer: EngineRenderedPeer(peer: peer1),
|
||||
threadInfo: nil,
|
||||
combinedReadState: readState,
|
||||
isRemovedFromTotalUnreadCount: false,
|
||||
presence: nil,
|
||||
hasUnseenMentions: false,
|
||||
hasUnseenReactions: false,
|
||||
draftState: nil,
|
||||
mediaDraftContentType: nil,
|
||||
inputActivities: nil,
|
||||
promoInfo: nil,
|
||||
ignoreUnreadBadge: false,
|
||||
displayAsMessage: false,
|
||||
hasFailedMessages: false,
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
storyState: nil,
|
||||
requiresPremiumForMessaging: false,
|
||||
displayAsTopicList: false,
|
||||
tags: []
|
||||
)), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
}
|
||||
|
||||
var itemNodes: [ChatListItemNode] = []
|
||||
for i in 0 ..< items.count {
|
||||
items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in
|
||||
if let itemNode = node as? ChatListItemNode {
|
||||
itemNodes.append(itemNode)
|
||||
}
|
||||
apply().1(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
}
|
||||
|
||||
self.maskNode.image = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
var currentY: CGFloat = 0.0
|
||||
let fakeLabelPlaceholderHeight: CGFloat = 8.0
|
||||
|
||||
func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) {
|
||||
let startPoint = origin
|
||||
let diameter = fakeLabelPlaceholderHeight
|
||||
context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter)))
|
||||
context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter)))
|
||||
}
|
||||
|
||||
while currentY < size.height {
|
||||
let sampleIndex = 0
|
||||
let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height
|
||||
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
|
||||
let textFrame = itemNodes[sampleIndex].textNode.textNode.frame.offsetBy(dx: 0.0, dy: currentY)
|
||||
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 60.0)
|
||||
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 120.0)
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX + 120.0 + 10.0, y: currentY + floor((itemHeight - fakeLabelPlaceholderHeight) / 2.0)), width: 60.0)
|
||||
|
||||
let dateFrame = itemNodes[sampleIndex].dateNode.frame.offsetBy(dx: 0.0, dy: currentY)
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: dateFrame.maxX - 30.0, y: dateFrame.minY + 4.0), width: 30.0)
|
||||
|
||||
currentY += itemHeight
|
||||
}
|
||||
})
|
||||
|
||||
self.effectNode.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
|
||||
self.effectNode.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
|
||||
}
|
||||
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNode {
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
@ -46,6 +303,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
private var extractedRect: CGRect?
|
||||
private var nonExtractedRect: CGRect?
|
||||
|
||||
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let bottomSeparatorNode: ASDisplayNode
|
||||
@ -57,6 +315,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
private var itemNode: ListViewItemNode?
|
||||
private var loadingOverlayNode: LoadingOverlayNode?
|
||||
|
||||
override init() {
|
||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||
@ -66,6 +325,10 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
self.extractedBackgroundImageNode.displaysAsynchronously = false
|
||||
self.extractedBackgroundImageNode.alpha = 0.0
|
||||
|
||||
var bringToFrontForHighlightImpl: (() -> Void)?
|
||||
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
|
||||
self.selectionNode.isUserInteractionEnabled = false
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
self.maskNode.isUserInteractionEnabled = false
|
||||
|
||||
@ -76,7 +339,12 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
|
||||
super.init()
|
||||
|
||||
bringToFrontForHighlightImpl = { [weak self] in
|
||||
self?.bringToFrontForHighlight?()
|
||||
}
|
||||
|
||||
self.addSubnode(self.bottomSeparatorNode)
|
||||
self.addSubnode(self.selectionNode)
|
||||
|
||||
self.containerNode.addSubnode(self.contextSourceNode)
|
||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||
@ -156,6 +424,12 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
self.item = item
|
||||
self.presentationData = presentationData
|
||||
self.theme = presentationData.theme
|
||||
|
||||
self.selectionNode.pressed = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item?.action()
|
||||
}
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 16.0 + safeInsets.left
|
||||
|
||||
@ -221,8 +495,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
},
|
||||
hidePsa: { _ in
|
||||
},
|
||||
activateChatPreview: { _, _, _, _, _ in
|
||||
},
|
||||
activateChatPreview: nil,
|
||||
present: { _ in
|
||||
},
|
||||
openForumThread: { _, _ in
|
||||
@ -245,7 +518,18 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
},
|
||||
hideChatFolderUpdates: {
|
||||
},
|
||||
openStories: { _, _ in
|
||||
openStories: { [weak self] _, sourceNode in
|
||||
guard let self, let item = self.item else {
|
||||
return
|
||||
}
|
||||
guard let itemNode = self.itemNode as? ChatListItemNode else {
|
||||
return
|
||||
}
|
||||
guard let controller = item.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
StoryContainerScreen.openPeerStories(context: item.context, peerId: item.data.peer.id, parentController: controller, avatarNode: itemNode.avatarNode)
|
||||
},
|
||||
dismissNotice: { _ in
|
||||
},
|
||||
@ -255,9 +539,11 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
|
||||
let index: EngineChatList.Item.Index
|
||||
let messages: [EngineMessage]
|
||||
if let message = item.data.topMessage {
|
||||
index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: message.index))
|
||||
messages = [message]
|
||||
let isLoading = item.data.isLoading
|
||||
|
||||
if !isLoading, !item.data.topMessages.isEmpty {
|
||||
index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: item.data.topMessages[0].index))
|
||||
messages = item.data.topMessages
|
||||
} else {
|
||||
index = EngineChatList.Item.Index.chatList(ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(id: MessageId(peerId: item.data.peer.id, namespace: Namespaces.Message.Cloud, id: 1), timestamp: 0)))
|
||||
messages = []
|
||||
@ -288,7 +574,12 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
forumTopicData: nil,
|
||||
topForumTopicItems: [],
|
||||
autoremoveTimeout: nil,
|
||||
storyState: nil,
|
||||
storyState: item.data.storyStats.flatMap { storyStats in
|
||||
return ChatListItemContent.StoryState(
|
||||
stats: storyStats,
|
||||
hasUnseenCloseFriends: false
|
||||
)
|
||||
},
|
||||
requiresPremiumForMessaging: false,
|
||||
displayAsTopicList: false,
|
||||
tags: [],
|
||||
@ -297,7 +588,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
searchQuery: nil,
|
||||
messageCount: nil,
|
||||
hideSeparator: true,
|
||||
hideDate: false
|
||||
hideDate: isLoading
|
||||
)
|
||||
)),
|
||||
editing: false,
|
||||
@ -356,10 +647,39 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
self.contextSourceNode.contentNode.addSubnode(itemNode)
|
||||
}
|
||||
}
|
||||
let itemFrame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
|
||||
if let itemNode = self.itemNode {
|
||||
itemNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
|
||||
itemNode.frame = itemFrame
|
||||
}
|
||||
|
||||
if let itemNode = self.itemNode, item.data.isLoading {
|
||||
let loadingOverlayNode: LoadingOverlayNode
|
||||
if let current = self.loadingOverlayNode {
|
||||
loadingOverlayNode = current
|
||||
} else {
|
||||
loadingOverlayNode = LoadingOverlayNode()
|
||||
self.loadingOverlayNode = loadingOverlayNode
|
||||
itemNode.supernode?.insertSubnode(loadingOverlayNode, aboveSubnode: itemNode)
|
||||
}
|
||||
loadingOverlayNode.frame = itemFrame
|
||||
loadingOverlayNode.update(
|
||||
context: item.context,
|
||||
size: itemFrame.size,
|
||||
isInlineMode: false,
|
||||
presentationData: presentationData,
|
||||
transition: .immediate
|
||||
)
|
||||
} else {
|
||||
if let loadingOverlayNode = self.loadingOverlayNode {
|
||||
self.loadingOverlayNode = nil
|
||||
loadingOverlayNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
|
||||
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
|
||||
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
|
||||
|
||||
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
|
||||
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
|
||||
|
||||
@ -394,5 +714,19 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
}
|
||||
|
||||
private func updateTouchesAtPoint(_ point: CGPoint?) {
|
||||
var isHighlighted = false
|
||||
if let point, let itemNode = self.itemNode as? ChatListItemNode {
|
||||
if !itemNode.avatarNode.view.convert(itemNode.avatarNode.view.bounds, to: self.view).contains(point) {
|
||||
isHighlighted = true
|
||||
} else if let item = self.item, item.data.storyStats == nil {
|
||||
isHighlighted = true
|
||||
}
|
||||
}
|
||||
|
||||
if isHighlighted {
|
||||
self.selectionNode.updateIsHighlighted(true)
|
||||
} else {
|
||||
self.selectionNode.updateIsHighlighted(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -231,13 +231,15 @@ final class TelegramGlobalSettings {
|
||||
final class PeerInfoPersonalChannelData: Equatable {
|
||||
let peer: EnginePeer
|
||||
let subscriberCount: Int?
|
||||
let topMessage: EngineMessage?
|
||||
let topMessages: [EngineMessage]
|
||||
let storyStats: PeerStoryStats?
|
||||
let isLoading: Bool
|
||||
|
||||
init(peer: EnginePeer, subscriberCount: Int?, topMessage: EngineMessage?, isLoading: Bool) {
|
||||
init(peer: EnginePeer, subscriberCount: Int?, topMessages: [EngineMessage], storyStats: PeerStoryStats?, isLoading: Bool) {
|
||||
self.peer = peer
|
||||
self.subscriberCount = subscriberCount
|
||||
self.topMessage = topMessage
|
||||
self.topMessages = topMessages
|
||||
self.storyStats = storyStats
|
||||
self.isLoading = isLoading
|
||||
}
|
||||
|
||||
@ -251,7 +253,10 @@ final class PeerInfoPersonalChannelData: Equatable {
|
||||
if lhs.subscriberCount != rhs.subscriberCount {
|
||||
return false
|
||||
}
|
||||
if lhs.topMessage != rhs.topMessage {
|
||||
if lhs.topMessages != rhs.topMessages {
|
||||
return false
|
||||
}
|
||||
if lhs.storyStats != rhs.storyStats {
|
||||
return false
|
||||
}
|
||||
if lhs.isLoading != rhs.isLoading {
|
||||
@ -555,13 +560,36 @@ private func peerInfoPersonalChannel(context: AccountContext, peerId: EnginePeer
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
//TODO:localize and keep updated
|
||||
return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: channelPeer.id, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 5, fixedCombinedReadStates: nil)
|
||||
|> map { view, _, _ -> PeerInfoPersonalChannelData? in
|
||||
let entry = view.entries.last
|
||||
|
||||
let polledChannel: Signal<Void, NoError> = Signal<Void, NoError>.single(Void())
|
||||
|> then(
|
||||
context.account.viewTracker.polledChannel(peerId: channelPeer.id)
|
||||
|> ignoreValues
|
||||
|> map { _ -> Void in
|
||||
}
|
||||
)
|
||||
|
||||
return combineLatest(
|
||||
context.account.postbox.aroundMessageHistoryViewForLocation(.peer(peerId: channelPeer.id, threadId: nil), anchor: .upperBound, ignoreMessagesInTimestampRange: nil, count: 10, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: Set(), tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .not(Namespaces.Message.allNonRegular), orderStatistics: []),
|
||||
context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.StoryStats(id: channelPeer.id)
|
||||
),
|
||||
polledChannel
|
||||
)
|
||||
|> map { viewData, storyStats, _ -> PeerInfoPersonalChannelData? in
|
||||
let (view, _, _) = viewData
|
||||
var messages: [EngineMessage] = []
|
||||
for i in (0 ..< view.entries.count).reversed() {
|
||||
if messages.isEmpty {
|
||||
messages.append(EngineMessage(view.entries[i].message))
|
||||
} else if messages[0].groupingKey == view.entries[i].message.groupingKey {
|
||||
messages.append(EngineMessage(view.entries[i].message))
|
||||
}
|
||||
}
|
||||
messages = messages.reversed()
|
||||
|
||||
var isLoading = false
|
||||
if entry == nil && view.isLoading {
|
||||
if messages.isEmpty && view.isLoading {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
@ -575,7 +603,8 @@ private func peerInfoPersonalChannel(context: AccountContext, peerId: EnginePeer
|
||||
return PeerInfoPersonalChannelData(
|
||||
peer: channelPeer,
|
||||
subscriberCount: mappedParticipantCount,
|
||||
topMessage: (entry?.message).flatMap(EngineMessage.init),
|
||||
topMessages: messages,
|
||||
storyStats: storyStats,
|
||||
isLoading: isLoading
|
||||
)
|
||||
}
|
||||
|
@ -591,6 +591,7 @@ private final class PeerInfoInteraction {
|
||||
let openBirthdatePrivacy: () -> Void
|
||||
let openPremiumGift: () -> Void
|
||||
let editingOpenPersonalChannel: () -> Void
|
||||
let getController: () -> ViewController?
|
||||
|
||||
init(
|
||||
openUsername: @escaping (String, Bool, Promise<Bool>?) -> Void,
|
||||
@ -651,7 +652,8 @@ private final class PeerInfoInteraction {
|
||||
openBioPrivacy: @escaping () -> Void,
|
||||
openBirthdatePrivacy: @escaping () -> Void,
|
||||
openPremiumGift: @escaping () -> Void,
|
||||
editingOpenPersonalChannel: @escaping () -> Void
|
||||
editingOpenPersonalChannel: @escaping () -> Void,
|
||||
getController: @escaping () -> ViewController?
|
||||
) {
|
||||
self.openUsername = openUsername
|
||||
self.openPhone = openPhone
|
||||
@ -712,6 +714,7 @@ private final class PeerInfoInteraction {
|
||||
self.openBirthdatePrivacy = openBirthdatePrivacy
|
||||
self.openPremiumGift = openPremiumGift
|
||||
self.editingOpenPersonalChannel = editingOpenPersonalChannel
|
||||
self.getController = getController
|
||||
}
|
||||
}
|
||||
|
||||
@ -1107,7 +1110,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
|
||||
if let personalChannel = data.personalChannel {
|
||||
personalChannelTitle = personalChannel.peer.compactDisplayTitle
|
||||
}
|
||||
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerPersonalChannel, label: .text(personalChannelTitle ?? "Add"), text: "Personal Channel", icon: nil, action: {
|
||||
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerPersonalChannel, label: .text(personalChannelTitle ?? "Add"), text: "Channel", icon: nil, action: {
|
||||
interaction.editingOpenPersonalChannel()
|
||||
}))
|
||||
}
|
||||
@ -1183,8 +1186,12 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
||||
label = presentationData.strings.Conversation_StatusSubscribers(Int32(subscriberCount))
|
||||
}
|
||||
//TODO:localize
|
||||
items[.personalChannel]?.append(PeerInfoScreenHeaderItem(id: 0, text: "PERSONAL CHANNEL", label: label))
|
||||
items[.personalChannel]?.append(PeerInfoScreenPersonalChannelItem(id: 1, context: context, data: personalChannel, requestLayout: { _ in
|
||||
items[.personalChannel]?.append(PeerInfoScreenHeaderItem(id: 0, text: "CHANNEL", label: label))
|
||||
items[.personalChannel]?.append(PeerInfoScreenPersonalChannelItem(id: 1, context: context, data: personalChannel, controller: { [weak interaction] in
|
||||
guard let interaction else {
|
||||
return nil
|
||||
}
|
||||
return interaction.getController()
|
||||
}, action: { [weak interaction] in
|
||||
guard let interaction else {
|
||||
return
|
||||
@ -2727,6 +2734,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
return
|
||||
}
|
||||
self.editingOpenPersonalChannel()
|
||||
},
|
||||
getController: { [weak self] in
|
||||
return self?.controller
|
||||
}
|
||||
)
|
||||
|
||||
@ -7664,12 +7674,26 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if initialData.channelId == channel?.peer.id {
|
||||
return
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let toastText: String
|
||||
var mappedChannel: TelegramPersonalChannel?
|
||||
if let channel {
|
||||
mappedChannel = TelegramPersonalChannel(peerId: channel.peer.id, subscriberCount: channel.subscriberCount.flatMap(Int32.init(clamping:)), topMessageId: nil)
|
||||
if initialData.channelId != nil {
|
||||
toastText = "Personal channel updated."
|
||||
} else {
|
||||
toastText = "Personal channel added."
|
||||
}
|
||||
} else {
|
||||
toastText = "Personal channel removed."
|
||||
}
|
||||
let _ = self.context.engine.accountData.updatePersonalChannel(personalChannel: mappedChannel).startStandalone()
|
||||
|
||||
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: toastText, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/ListItemSwipeOptionContainer",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/ShareController",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -20,6 +20,7 @@ final class BusinessLinkListItemComponent: Component {
|
||||
let link: TelegramBusinessChatLinks.Link
|
||||
let action: () -> Void
|
||||
let deleteAction: () -> Void
|
||||
let shareAction: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@ -27,7 +28,8 @@ final class BusinessLinkListItemComponent: Component {
|
||||
strings: PresentationStrings,
|
||||
link: TelegramBusinessChatLinks.Link,
|
||||
action: @escaping () -> Void,
|
||||
deleteAction: @escaping () -> Void
|
||||
deleteAction: @escaping () -> Void,
|
||||
shareAction: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -35,6 +37,7 @@ final class BusinessLinkListItemComponent: Component {
|
||||
self.link = link
|
||||
self.action = action
|
||||
self.deleteAction = deleteAction
|
||||
self.shareAction = shareAction
|
||||
}
|
||||
|
||||
static func ==(lhs: BusinessLinkListItemComponent, rhs: BusinessLinkListItemComponent) -> Bool {
|
||||
@ -98,7 +101,11 @@ final class BusinessLinkListItemComponent: Component {
|
||||
return
|
||||
}
|
||||
self.swipeOptionContainer.setRevealOptionsOpened(false, animated: true)
|
||||
component.deleteAction()
|
||||
if option.key == AnyHashable(0 as Int) {
|
||||
component.shareAction()
|
||||
} else {
|
||||
component.deleteAction()
|
||||
}
|
||||
}
|
||||
|
||||
self.addSubview(self.swipeOptionContainer)
|
||||
@ -237,15 +244,21 @@ final class BusinessLinkListItemComponent: Component {
|
||||
self.swipeOptionContainer.updateLayout(size: swipeOptionContainerFrame.size, leftInset: 0.0, rightInset: 0.0)
|
||||
|
||||
var rightOptions: [ListItemSwipeOptionContainer.Option] = []
|
||||
let color: UIColor = component.theme.list.itemDisclosureActions.destructive.fillColor
|
||||
let textColor: UIColor = component.theme.list.itemDisclosureActions.destructive.foregroundColor
|
||||
//TODO:localize
|
||||
rightOptions = [
|
||||
ListItemSwipeOptionContainer.Option(
|
||||
key: 0,
|
||||
title: "Share",
|
||||
icon: .none,
|
||||
color: component.theme.list.itemDisclosureActions.accent.fillColor,
|
||||
textColor: component.theme.list.itemDisclosureActions.accent.foregroundColor
|
||||
),
|
||||
ListItemSwipeOptionContainer.Option(
|
||||
key: 1,
|
||||
title: component.strings.Common_Delete,
|
||||
icon: .none,
|
||||
color: color,
|
||||
textColor: textColor
|
||||
color: component.theme.list.itemDisclosureActions.destructive.fillColor,
|
||||
textColor: component.theme.list.itemDisclosureActions.destructive.foregroundColor
|
||||
)
|
||||
]
|
||||
self.swipeOptionContainer.setRevealOptions(([], rightOptions))
|
||||
|
@ -17,6 +17,7 @@ import ListActionItemComponent
|
||||
import BundleIconComponent
|
||||
import TextFormat
|
||||
import UndoUI
|
||||
import ShareController
|
||||
|
||||
final class BusinessLinksSetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -164,7 +165,7 @@ final class BusinessLinksSetupScreenComponent: Component {
|
||||
self.isCreatingLink = false
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
self.openLink(link: link)
|
||||
self.openLink(link: link, openKeyboard: true)
|
||||
}, error: { [weak self] error in
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
@ -192,11 +193,11 @@ final class BusinessLinksSetupScreenComponent: Component {
|
||||
|
||||
private func openLink(url: String) {
|
||||
if let link = self.links.first(where: { $0.url == url }) {
|
||||
self.openLink(link: link)
|
||||
self.openLink(link: link, openKeyboard: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func openLink(link: TelegramBusinessChatLinks.Link) {
|
||||
private func openLink(link: TelegramBusinessChatLinks.Link, openKeyboard: Bool) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
@ -212,6 +213,9 @@ final class BusinessLinksSetupScreenComponent: Component {
|
||||
botStart: nil,
|
||||
mode: .standard(.default)
|
||||
)
|
||||
if openKeyboard {
|
||||
chatController.activateInput(type: .text)
|
||||
}
|
||||
chatController.navigationPresentation = .modal
|
||||
self.environment?.controller()?.push(chatController)
|
||||
}
|
||||
@ -242,6 +246,18 @@ final class BusinessLinksSetupScreenComponent: Component {
|
||||
self.environment?.controller()?.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
|
||||
private func openShareLink(url: String) {
|
||||
guard let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let link = self.links.first(where: { $0.url == url }) else {
|
||||
return
|
||||
}
|
||||
|
||||
environment.controller()?.present(ShareController(context: component.context, subject: .url(link.url), showInChat: nil, externalShare: false, immediateExternalShare: false), in: .window(.root))
|
||||
}
|
||||
|
||||
func update(component: BusinessLinksSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -515,6 +531,12 @@ final class BusinessLinksSetupScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
self.openDeleteLink(url: link.url)
|
||||
},
|
||||
shareAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openShareLink(url: link.url)
|
||||
}
|
||||
))))
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ swift_library(
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/ItemListPeerActionItem",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/Components/HierarchyTrackingLayer",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -0,0 +1,263 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import HierarchyTrackingLayer
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ContactsPeerItem
|
||||
import ItemListUI
|
||||
import TelegramCore
|
||||
|
||||
final class ShimmerEffectView: UIView {
|
||||
private var currentBackgroundColor: UIColor?
|
||||
private var currentForegroundColor: UIColor?
|
||||
private let imageViewContainer: UIView
|
||||
private let imageView: UIImageView
|
||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
private var isCurrentlyInHierarchy = false
|
||||
private var shouldBeAnimating = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||
|
||||
self.imageViewContainer = UIView()
|
||||
|
||||
self.imageView = UIImageView()
|
||||
self.imageView.contentMode = .scaleToFill
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.hierarchyTrackingLayer)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.imageViewContainer.addSubview(self.imageView)
|
||||
self.addSubview(self.imageViewContainer)
|
||||
|
||||
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
|
||||
self?.didEnterHierarchy()
|
||||
}
|
||||
self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
|
||||
self?.didExitHierarchy()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func didEnterHierarchy() {
|
||||
self.isCurrentlyInHierarchy = true
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
private func didExitHierarchy() {
|
||||
self.isCurrentlyInHierarchy = false
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
|
||||
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
|
||||
return
|
||||
}
|
||||
self.currentBackgroundColor = backgroundColor
|
||||
self.currentForegroundColor = foregroundColor
|
||||
|
||||
self.imageView.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.clip(to: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
|
||||
let peakColor = foregroundColor.cgColor
|
||||
|
||||
var locations: [CGFloat] = [0.0, 0.5, 1.0]
|
||||
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
|
||||
return
|
||||
}
|
||||
let sizeUpdated = self.absoluteLocation?.1 != containerSize
|
||||
let frameUpdated = self.absoluteLocation?.0 != rect
|
||||
self.absoluteLocation = (rect, containerSize)
|
||||
|
||||
if sizeUpdated {
|
||||
if self.shouldBeAnimating {
|
||||
self.imageView.layer.removeAnimation(forKey: "shimmer")
|
||||
self.addImageAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
if frameUpdated {
|
||||
self.imageViewContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
|
||||
}
|
||||
|
||||
self.updateAnimation()
|
||||
}
|
||||
|
||||
private func updateAnimation() {
|
||||
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
|
||||
if shouldBeAnimating != self.shouldBeAnimating {
|
||||
self.shouldBeAnimating = shouldBeAnimating
|
||||
if shouldBeAnimating {
|
||||
self.addImageAnimation()
|
||||
} else {
|
||||
self.imageView.layer.removeAnimation(forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addImageAnimation() {
|
||||
guard let containerSize = self.absoluteLocation?.1 else {
|
||||
return
|
||||
}
|
||||
let gradientHeight: CGFloat = 250.0
|
||||
self.imageView.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight))
|
||||
let animation = self.imageView.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
|
||||
animation.repeatCount = Float.infinity
|
||||
animation.beginTime = 1.0
|
||||
self.imageView.layer.add(animation, forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
|
||||
final class PeerSelectionLoadingView: UIView {
|
||||
private let backgroundColorView: UIView
|
||||
private let effectView: ShimmerEffectView
|
||||
private let maskImageView: UIImageView
|
||||
private var currentParams: (size: CGSize, presentationData: PresentationData)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundColorView = UIView()
|
||||
self.effectView = ShimmerEffectView()
|
||||
self.maskImageView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubview(self.backgroundColorView)
|
||||
self.addSubview(self.effectView)
|
||||
self.addSubview(self.maskImageView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func update(context: AccountContext, size: CGSize, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
|
||||
if self.currentParams?.size != size || self.currentParams?.presentationData !== presentationData {
|
||||
self.currentParams = (size, presentationData)
|
||||
|
||||
let peer1: EnginePeer = .user(TelegramUser(id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "FirstName", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil))
|
||||
|
||||
let items = (0 ..< 1).map { _ -> ContactsPeerItem in
|
||||
return ContactsPeerItem(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
style: .plain,
|
||||
sectionId: 0,
|
||||
sortOrder: .firstLast,
|
||||
displayOrder: .firstLast,
|
||||
context: context,
|
||||
peerMode: .peer,
|
||||
peer: .peer(peer: peer1, chatPeer: peer1),
|
||||
status: .custom(string: "status", multiline: false, isActive: false, icon: nil),
|
||||
badge: nil,
|
||||
requiresPremiumForMessaging: false,
|
||||
enabled: true,
|
||||
selection: .none,
|
||||
selectionPosition: .left,
|
||||
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
|
||||
options: [],
|
||||
additionalActions: [],
|
||||
actionIcon: .none,
|
||||
index: nil,
|
||||
header: nil,
|
||||
action: { _ in },
|
||||
disabledAction: nil,
|
||||
setPeerIdWithRevealedOptions: nil,
|
||||
deletePeer: nil,
|
||||
itemHighlighting: nil,
|
||||
contextAction: nil,
|
||||
arrowAction: nil,
|
||||
animationCache: nil,
|
||||
animationRenderer: nil,
|
||||
storyStats: nil,
|
||||
openStories: nil
|
||||
)
|
||||
}
|
||||
|
||||
var itemNodes: [ContactsPeerItemNode] = []
|
||||
for i in 0 ..< items.count {
|
||||
items[i].nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 100.0), synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: (i == items.count - 1) ? nil : items[i + 1], completion: { node, apply in
|
||||
if let itemNode = node as? ContactsPeerItemNode {
|
||||
itemNodes.append(itemNode)
|
||||
}
|
||||
apply().1(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
}
|
||||
|
||||
self.backgroundColorView.backgroundColor = presentationData.theme.list.mediaPlaceholderColor
|
||||
|
||||
let maskSize = CGSize(width: size.width, height: round(size.height / itemNodes[0].contentSize.height) * itemNodes[0].contentSize.height)
|
||||
|
||||
self.maskImageView.image = generateImage(size, rotatedContext: { size, context in
|
||||
context.setFillColor(presentationData.theme.chatList.backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let size = maskSize
|
||||
|
||||
var currentY: CGFloat = 0.0
|
||||
let fakeLabelPlaceholderHeight: CGFloat = 8.0
|
||||
|
||||
func fillLabelPlaceholderRect(origin: CGPoint, width: CGFloat) {
|
||||
let startPoint = origin
|
||||
let diameter = fakeLabelPlaceholderHeight
|
||||
context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter)))
|
||||
context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter)))
|
||||
}
|
||||
|
||||
while currentY < size.height {
|
||||
let sampleIndex = 0
|
||||
let itemHeight: CGFloat = itemNodes[sampleIndex].contentSize.height
|
||||
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
|
||||
if !itemNodes[sampleIndex].avatarNode.isHidden {
|
||||
context.fillEllipse(in: itemNodes[sampleIndex].avatarNode.view.convert(itemNodes[sampleIndex].avatarNode.bounds, to: itemNodes[sampleIndex].view).offsetBy(dx: 0.0, dy: currentY))
|
||||
}
|
||||
|
||||
let titleFrame = itemNodes[sampleIndex].titleNode.frame.offsetBy(dx: 0.0, dy: currentY)
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: titleFrame.minX, y: floor(titleFrame.midY - fakeLabelPlaceholderHeight / 2.0)), width: 100.0)
|
||||
|
||||
let textFrame = itemNodes[sampleIndex].statusNode.frame.offsetBy(dx: 0.0, dy: currentY)
|
||||
fillLabelPlaceholderRect(origin: CGPoint(x: textFrame.minX, y: currentY + itemHeight - floor(itemNodes[sampleIndex].titleNode.frame.midY - fakeLabelPlaceholderHeight / 2.0) - fakeLabelPlaceholderHeight), width: 40.0)
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(presentationData.theme.chatList.itemSeparatorColor.cgColor)
|
||||
context.fill(itemNodes[sampleIndex].separatorNode.frame.offsetBy(dx: 0.0, dy: currentY))
|
||||
|
||||
currentY += itemHeight
|
||||
}
|
||||
})
|
||||
|
||||
self.effectView.update(backgroundColor: presentationData.theme.list.mediaPlaceholderColor, foregroundColor: presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4))
|
||||
self.effectView.updateAbsoluteRect(CGRect(origin: CGPoint(), size: size), within: size)
|
||||
}
|
||||
transition.updateFrame(view: self.backgroundColorView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
transition.updateFrame(view: self.maskImageView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size))
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import ComponentFlow
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import ItemListPeerActionItem
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
final class PeerSelectionScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -202,6 +203,7 @@ final class PeerSelectionScreenComponent: Component {
|
||||
private var emptyState: ComponentView<Empty>?
|
||||
private var contentListNode: ContentListNode?
|
||||
private var emptySearchState: ComponentView<Empty>?
|
||||
private var loadingView: PeerSelectionLoadingView?
|
||||
|
||||
private let navigationBarView = ComponentView<Empty>()
|
||||
private var navigationHeight: CGFloat?
|
||||
@ -214,7 +216,7 @@ final class PeerSelectionScreenComponent: Component {
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var channels: [PeerSelectionScreen.ChannelInfo] = []
|
||||
private var channels: [PeerSelectionScreen.ChannelInfo]?
|
||||
private var channelsDisposable: Disposable?
|
||||
|
||||
private var isSearchDisplayControllerActive: Bool = false
|
||||
@ -248,7 +250,7 @@ final class PeerSelectionScreenComponent: Component {
|
||||
}
|
||||
|
||||
if let peer {
|
||||
guard let channel = self.channels.first(where: { $0.peer.id == peer.id }) else {
|
||||
guard let channel = self.channels?.first(where: { $0.peer.id == peer.id }) else {
|
||||
return
|
||||
}
|
||||
component.completion(channel)
|
||||
@ -279,7 +281,7 @@ final class PeerSelectionScreenComponent: Component {
|
||||
navigationBackTitle: nil,
|
||||
titleComponent: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Personal Channel", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor))
|
||||
text: .plain(NSAttributedString(string: "Channel", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor))
|
||||
))),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "select your channel", font: Font.regular(12.0), textColor: theme.rootController.navigationBar.secondaryTextColor))
|
||||
@ -395,6 +397,10 @@ final class PeerSelectionScreenComponent: Component {
|
||||
crossfadeStoryPeers: false
|
||||
)))
|
||||
}
|
||||
|
||||
if let contentListNode = self.contentListNode, let loadingView = self.loadingView {
|
||||
transition.setFrame(view: loadingView, frame: contentListNode.frame.offsetBy(dx: 0.0, dy: -offset + contentListNode.insets.top))
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: PeerSelectionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
@ -563,20 +569,22 @@ final class PeerSelectionScreenComponent: Component {
|
||||
if component.initialData.channelId != nil && self.searchQuery.isEmpty {
|
||||
entries.append(.hide)
|
||||
}
|
||||
for channel in self.channels {
|
||||
if !self.searchQuery.isEmpty {
|
||||
var matches = false
|
||||
if let channels = self.channels {
|
||||
for channel in channels {
|
||||
if !self.searchQuery.isEmpty {
|
||||
var matches = false
|
||||
inner: for nameComponent in channel.peer.compactDisplayTitle.lowercased().components(separatedBy: self.searchQueryComponentSeparationCharacterSet) {
|
||||
if nameComponent.lowercased().hasPrefix(self.searchQuery) {
|
||||
matches = true
|
||||
break inner
|
||||
}
|
||||
}
|
||||
if !matches {
|
||||
continue
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
}
|
||||
entries.append(.item(peer: channel.peer, subscriberCount: channel.subscriberCount, sortIndex: entries.count))
|
||||
}
|
||||
entries.append(.item(peer: channel.peer, subscriberCount: channel.subscriberCount, sortIndex: entries.count))
|
||||
}
|
||||
contentListNode.setEntries(entries: entries, animated: !transition.animation.isImmediate)
|
||||
|
||||
@ -619,6 +627,32 @@ final class PeerSelectionScreenComponent: Component {
|
||||
emptySearchState.view?.removeFromSuperview()
|
||||
}
|
||||
|
||||
if self.channels == nil, let contentListNode = self.contentListNode {
|
||||
let loadingView: PeerSelectionLoadingView
|
||||
if let current = self.loadingView {
|
||||
loadingView = current
|
||||
} else {
|
||||
loadingView = PeerSelectionLoadingView()
|
||||
self.loadingView = loadingView
|
||||
contentListNode.view.superview?.insertSubview(loadingView, aboveSubview: contentListNode.view)
|
||||
}
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
loadingView.update(
|
||||
context: component.context,
|
||||
size: CGSize(width: contentListNode.bounds.size.width, height: floor(contentListNode.bounds.size.height * 1.2)),
|
||||
presentationData: presentationData,
|
||||
transition: transition.containedViewLayoutTransition
|
||||
)
|
||||
} else {
|
||||
if let loadingView = self.loadingView {
|
||||
self.loadingView = nil
|
||||
let removeTransition: Transition = .easeInOut(duration: 0.2)
|
||||
removeTransition.setAlpha(view: loadingView, alpha: 0.0, completion: { [weak loadingView] _ in
|
||||
loadingView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.updateNavigationScrolling(navigationHeight: navigationHeight, transition: transition)
|
||||
|
||||
if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
|
||||
@ -652,7 +686,7 @@ final class PeerSelectionScreenComponent: Component {
|
||||
|
||||
public final class PeerSelectionScreen: ViewControllerComponentContainer {
|
||||
public final class InitialData {
|
||||
fileprivate let channelId: EnginePeer.Id?
|
||||
public let channelId: EnginePeer.Id?
|
||||
|
||||
init(channelId: EnginePeer.Id?) {
|
||||
self.channelId = channelId
|
||||
|
@ -220,7 +220,17 @@ func updateChatPresentationInterfaceStateImpl(
|
||||
}
|
||||
}
|
||||
|
||||
if let (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) {
|
||||
var canHaveUrlPreview = true
|
||||
if case let .customChatContents(customChatContents) = updatedChatPresentationInterfaceState.subject {
|
||||
switch customChatContents.kind {
|
||||
case .quickReplyMessageInput:
|
||||
break
|
||||
case .businessLinkSetup:
|
||||
canHaveUrlPreview = false
|
||||
}
|
||||
}
|
||||
|
||||
if canHaveUrlPreview, let (updatedUrlPreviewState, updatedUrlPreviewSignal) = urlPreviewStateForInputText(updatedChatPresentationInterfaceState.interfaceState.composeInputState.inputText, context: selfController.context, currentQuery: selfController.urlPreviewQueryState?.0, forPeerId: selfController.chatLocation.peerId) {
|
||||
selfController.urlPreviewQueryState?.1.dispose()
|
||||
var inScope = true
|
||||
var inScopeResult: ((TelegramMediaWebpage?) -> (TelegramMediaWebpage, String)?)?
|
||||
|
@ -763,7 +763,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
case let .businessLinkSetup(link):
|
||||
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
|
||||
let entities = generateTextEntities(inputText.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(inputText, maxAnimatedEmojisInText: 0))
|
||||
let entities = generateChatInputTextEntities(inputText, generateLinks: false)
|
||||
|
||||
let message = inputText.string
|
||||
|
||||
@ -12127,7 +12127,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
let _ = self.context.engine.peers.checkPeerChatServiceActions(peerId: peerId).startStandalone()
|
||||
}
|
||||
|
||||
if self.chatDisplayNode.frameForInputActionButton() != nil {
|
||||
if self.chatLocation.peerId != nil && self.chatDisplayNode.frameForInputActionButton() != nil {
|
||||
let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.string
|
||||
if !inputText.isEmpty {
|
||||
if inputText.count > 4 {
|
||||
@ -16319,7 +16319,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
|
||||
func activateInput(type: ChatControllerActivateInput) {
|
||||
public func activateInput(type: ChatControllerActivateInput) {
|
||||
if self.didAppear {
|
||||
switch type {
|
||||
case .text:
|
||||
|
@ -3968,7 +3968,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
for text in breakChatInputText(trimChatInputText(inputText)) {
|
||||
if text.length != 0 {
|
||||
var attributes: [MessageAttribute] = []
|
||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0))
|
||||
let entities: [MessageTextEntity]
|
||||
if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject, case .businessLinkSetup = customChatContents.kind {
|
||||
entities = generateChatInputTextEntities(text, generateLinks: false)
|
||||
} else {
|
||||
entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0))
|
||||
}
|
||||
if !entities.isEmpty {
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||
}
|
||||
|
@ -1065,6 +1065,7 @@ func openResolvedUrlImpl(
|
||||
context: context,
|
||||
chatLocation: .peer(link.peer),
|
||||
updateTextInputState: ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(link.message, entities: link.entities)),
|
||||
activateInput: .text,
|
||||
keepStack: .always
|
||||
))
|
||||
}
|
||||
|
@ -178,9 +178,11 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
|
||||
}
|
||||
})
|
||||
|
||||
for entity in generateTextEntities(text.string, enabledTypes: .allUrl) {
|
||||
if case .Url = entity.type {
|
||||
entities.append(entity)
|
||||
if generateLinks {
|
||||
for entity in generateTextEntities(text.string, enabledTypes: .allUrl) {
|
||||
if case .Url = entity.type {
|
||||
entities.append(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user