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
4b1b272081
commit
b25f8ed37f
@ -11362,7 +11362,7 @@ Sorry for the inconvenience.";
|
|||||||
"Business.QuickReplies" = "Quick Replies";
|
"Business.QuickReplies" = "Quick Replies";
|
||||||
"Business.GreetingMessages" = "Greeting Messages";
|
"Business.GreetingMessages" = "Greeting Messages";
|
||||||
"Business.AwayMessages" = "Away Messages";
|
"Business.AwayMessages" = "Away Messages";
|
||||||
"Business.Chatbots" = "Chatbots";
|
"Business.ChatbotsItem" = "Chatbots";
|
||||||
"Business.Intro" = "Intro";
|
"Business.Intro" = "Intro";
|
||||||
|
|
||||||
"Business.LocationInfo" = "Display the location of your business on your account.";
|
"Business.LocationInfo" = "Display the location of your business on your account.";
|
||||||
@ -11409,6 +11409,7 @@ Sorry for the inconvenience.";
|
|||||||
"ChatList.ItemMenuEdit" = "Edit";
|
"ChatList.ItemMenuEdit" = "Edit";
|
||||||
"ChatList.ItemMenuDelete" = "Delete";
|
"ChatList.ItemMenuDelete" = "Delete";
|
||||||
"ChatList.PeerTypeNonContact" = "non-contact";
|
"ChatList.PeerTypeNonContact" = "non-contact";
|
||||||
|
"ChatList.PeerTypeNonContactUser" = "non-contact";
|
||||||
|
|
||||||
"ChatListFilter.TagLabelNoTag" = "NO TAG";
|
"ChatListFilter.TagLabelNoTag" = "NO TAG";
|
||||||
"ChatListFilter.TagLabelPremiumExpired" = "PREMIUM EXPIRED";
|
"ChatListFilter.TagLabelPremiumExpired" = "PREMIUM EXPIRED";
|
||||||
@ -11575,7 +11576,7 @@ Sorry for the inconvenience.";
|
|||||||
"BusinessLocationSetup.AlertUnsavedChanges.Text" = "You have unsaved changes.";
|
"BusinessLocationSetup.AlertUnsavedChanges.Text" = "You have unsaved changes.";
|
||||||
"BusinessLocationSetup.AlertUnsavedChanges.ResetAction" = "Revert";
|
"BusinessLocationSetup.AlertUnsavedChanges.ResetAction" = "Revert";
|
||||||
|
|
||||||
"ChatbotSetup.Title" = "Chatbots";
|
"ChatbotSetup.TitleItem" = "Chatbots";
|
||||||
"ChatbotSetup.Text" = "Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More >]()";
|
"ChatbotSetup.Text" = "Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More >]()";
|
||||||
"ChatbotSetup.TextLink" = "https://telegram.org";
|
"ChatbotSetup.TextLink" = "https://telegram.org";
|
||||||
|
|
||||||
|
@ -4165,14 +4165,14 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
|
|||||||
if isContact {
|
if isContact {
|
||||||
return (strings.ChatList_PeerTypeContact, false, false, nil)
|
return (strings.ChatList_PeerTypeContact, false, false, nil)
|
||||||
} else {
|
} else {
|
||||||
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
|
return (strings.ChatList_PeerTypeNonContactUser, false, false, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if case .secretChat = peer {
|
} else if case .secretChat = peer {
|
||||||
if isContact {
|
if isContact {
|
||||||
return (strings.ChatList_PeerTypeContact, false, false, nil)
|
return (strings.ChatList_PeerTypeContact, false, false, nil)
|
||||||
} else {
|
} else {
|
||||||
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
|
return (strings.ChatList_PeerTypeNonContactUser, false, false, nil)
|
||||||
}
|
}
|
||||||
} else if case .legacyGroup = peer {
|
} else if case .legacyGroup = peer {
|
||||||
return (strings.ChatList_PeerTypeGroup, false, false, nil)
|
return (strings.ChatList_PeerTypeGroup, false, false, nil)
|
||||||
@ -4183,7 +4183,7 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
|
|||||||
return (strings.ChatList_PeerTypeChannel, false, false, nil)
|
return (strings.ChatList_PeerTypeChannel, false, false, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
|
return (strings.ChatList_PeerTypeNonContactUser, false, false, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
|
public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
|
||||||
|
@ -633,7 +633,7 @@ public enum PremiumPerk: CaseIterable {
|
|||||||
case .businessAwayMessage:
|
case .businessAwayMessage:
|
||||||
return strings.Business_AwayMessages
|
return strings.Business_AwayMessages
|
||||||
case .businessChatBots:
|
case .businessChatBots:
|
||||||
return strings.Business_Chatbots
|
return strings.Business_ChatbotsItem
|
||||||
case .businessIntro:
|
case .businessIntro:
|
||||||
return strings.Business_Intro
|
return strings.Business_Intro
|
||||||
}
|
}
|
||||||
|
@ -971,7 +971,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
videoFile: videos["business_bots"],
|
videoFile: videos["business_bots"],
|
||||||
decoration: .business
|
decoration: .business
|
||||||
)),
|
)),
|
||||||
title: strings.Business_Chatbots,
|
title: strings.Business_ChatbotsItem,
|
||||||
text: strings.Business_ChatbotsInfo,
|
text: strings.Business_ChatbotsInfo,
|
||||||
textColor: textColor
|
textColor: textColor
|
||||||
)
|
)
|
||||||
|
@ -600,7 +600,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes
|
|||||||
extension StoreMessage {
|
extension StoreMessage {
|
||||||
convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
|
convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) {
|
||||||
switch apiMessage {
|
switch apiMessage {
|
||||||
case let .message(flags, _, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId):
|
case let .message(flags, flags2, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId):
|
||||||
let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId
|
let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId
|
||||||
|
|
||||||
var namespace = namespace
|
var namespace = namespace
|
||||||
@ -906,7 +906,7 @@ extension StoreMessage {
|
|||||||
storeFlags.insert(.IsForumTopic)
|
storeFlags.insert(.IsForumTopic)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags & (1 << 4)) != 0 || (flags & (1 << 13)) != 0 {
|
if (flags & (1 << 4)) != 0 || (flags & (1 << 13)) != 0 || (flags2 & (1 << 1)) != 0 {
|
||||||
var notificationFlags: NotificationInfoMessageAttributeFlags = []
|
var notificationFlags: NotificationInfoMessageAttributeFlags = []
|
||||||
if (flags & (1 << 4)) != 0 {
|
if (flags & (1 << 4)) != 0 {
|
||||||
notificationFlags.insert(.personal)
|
notificationFlags.insert(.personal)
|
||||||
@ -916,6 +916,9 @@ extension StoreMessage {
|
|||||||
if (flags & (1 << 13)) != 0 {
|
if (flags & (1 << 13)) != 0 {
|
||||||
notificationFlags.insert(.muted)
|
notificationFlags.insert(.muted)
|
||||||
}
|
}
|
||||||
|
if (flags2 & (1 << 1)) != 0 {
|
||||||
|
notificationFlags.insert(.automaticMessage)
|
||||||
|
}
|
||||||
attributes.append(NotificationInfoMessageAttribute(flags: notificationFlags))
|
attributes.append(NotificationInfoMessageAttribute(flags: notificationFlags))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3452,7 +3452,17 @@ func replayFinalState(
|
|||||||
if message.flags.contains(.Incoming) {
|
if message.flags.contains(.Incoming) {
|
||||||
addedOperationIncomingMessageIds.append(id)
|
addedOperationIncomingMessageIds.append(id)
|
||||||
if let authorId = message.authorId {
|
if let authorId = message.authorId {
|
||||||
recordPeerActivityTimestamp(peerId: authorId, timestamp: message.timestamp, into: &peerActivityTimestamps)
|
var isAutomatic = false
|
||||||
|
for attribute in message.attributes {
|
||||||
|
if let attribute = attribute as? NotificationInfoMessageAttribute {
|
||||||
|
if attribute.flags.contains(.automaticMessage) {
|
||||||
|
isAutomatic = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isAutomatic {
|
||||||
|
recordPeerActivityTimestamp(peerId: authorId, timestamp: message.timestamp, into: &peerActivityTimestamps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if message.flags.contains(.WasScheduled) {
|
if message.flags.contains(.WasScheduled) {
|
||||||
|
@ -12,9 +12,9 @@ public struct NotificationInfoMessageAttributeFlags: OptionSet {
|
|||||||
self.rawValue = 0
|
self.rawValue = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
public static let muted = NotificationInfoMessageAttributeFlags(rawValue: 1)
|
public static let muted = NotificationInfoMessageAttributeFlags(rawValue: 1 << 0)
|
||||||
public static let personal = NotificationInfoMessageAttributeFlags(rawValue: 2)
|
public static let personal = NotificationInfoMessageAttributeFlags(rawValue: 1 << 1)
|
||||||
|
public static let automaticMessage = NotificationInfoMessageAttributeFlags(rawValue: 1 << 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotificationInfoMessageAttribute: MessageAttribute {
|
public class NotificationInfoMessageAttribute: MessageAttribute {
|
||||||
|
@ -1613,34 +1613,6 @@ public extension TelegramEngine.EngineData.Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct BusinessIntro: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
|
|
||||||
public typealias Result = CachedTelegramBusinessIntro?
|
|
||||||
|
|
||||||
fileprivate var id: EnginePeer.Id
|
|
||||||
public var mapKey: EnginePeer.Id {
|
|
||||||
return self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(id: EnginePeer.Id) {
|
|
||||||
self.id = id
|
|
||||||
}
|
|
||||||
|
|
||||||
var key: PostboxViewKey {
|
|
||||||
return .cachedPeerData(peerId: self.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func extract(view: PostboxView) -> Result {
|
|
||||||
guard let view = view as? CachedPeerDataView else {
|
|
||||||
preconditionFailure()
|
|
||||||
}
|
|
||||||
if let cachedData = view.cachedPeerData as? CachedUserData {
|
|
||||||
return cachedData.businessIntro
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ChatManagingBot: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
|
public struct ChatManagingBot: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
|
||||||
public typealias Result = PeerStatusSettings.ManagingBot?
|
public typealias Result = PeerStatusSettings.ManagingBot?
|
||||||
|
|
||||||
|
@ -371,6 +371,9 @@ func _internal_toggleChatManagingBotIsPaused(account: Account, chatId: EnginePee
|
|||||||
if let managingBot = peerStatusSettings.managingBot {
|
if let managingBot = peerStatusSettings.managingBot {
|
||||||
isPaused = !managingBot.isPaused
|
isPaused = !managingBot.isPaused
|
||||||
peerStatusSettings.managingBot?.isPaused = isPaused
|
peerStatusSettings.managingBot?.isPaused = isPaused
|
||||||
|
if !isPaused {
|
||||||
|
peerStatusSettings.managingBot?.canReply = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return current.withUpdatedPeerStatusSettings(peerStatusSettings)
|
return current.withUpdatedPeerStatusSettings(peerStatusSettings)
|
||||||
@ -412,6 +415,35 @@ func _internal_removeChatManagingBot(account: Account, chatId: EnginePeer.Id) ->
|
|||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in
|
||||||
|
guard let current = current as? CachedUserData else {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
if let connectedBot = current.connectedBot {
|
||||||
|
var additionalPeers = connectedBot.recipients.additionalPeers
|
||||||
|
var excludePeers = connectedBot.recipients.excludePeers
|
||||||
|
if connectedBot.recipients.exclude {
|
||||||
|
additionalPeers.insert(chatId)
|
||||||
|
} else {
|
||||||
|
additionalPeers.remove(chatId)
|
||||||
|
excludePeers.insert(chatId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return current.withUpdatedConnectedBot(TelegramAccountConnectedBot(
|
||||||
|
id: connectedBot.id,
|
||||||
|
recipients: TelegramBusinessRecipients(
|
||||||
|
categories: connectedBot.recipients.categories,
|
||||||
|
additionalPeers: additionalPeers,
|
||||||
|
excludePeers: excludePeers,
|
||||||
|
exclude: connectedBot.recipients.exclude
|
||||||
|
),
|
||||||
|
canReply: connectedBot.canReply
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|> mapToSignal { _ -> Signal<Never, NoError> in
|
|> mapToSignal { _ -> Signal<Never, NoError> in
|
||||||
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||||
|
@ -811,7 +811,7 @@ extension TelegramBusinessRecipients {
|
|||||||
self.init(
|
self.init(
|
||||||
categories: categories,
|
categories: categories,
|
||||||
additionalPeers: Set((users ?? []).map( { PeerId(namespace: Namespaces.Peer.CloudUser, id: ._internalFromInt64Value($0)) })),
|
additionalPeers: Set((users ?? []).map( { PeerId(namespace: Namespaces.Peer.CloudUser, id: ._internalFromInt64Value($0)) })),
|
||||||
excludePeers: Set((excludeUsers ?? []).map(PeerId.init)),
|
excludePeers: Set((excludeUsers ?? []).map( { PeerId(namespace: Namespaces.Peer.CloudUser, id: ._internalFromInt64Value($0)) })),
|
||||||
exclude: (flags & (1 << 5)) != 0
|
exclude: (flags & (1 << 5)) != 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1175,6 +1175,250 @@ private enum ChatEmptyNodeContentType: Equatable {
|
|||||||
case premiumRequired
|
case premiumRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode {
|
||||||
|
private struct Params: Equatable {
|
||||||
|
var theme: PresentationTheme
|
||||||
|
var strings: PresentationStrings
|
||||||
|
var chatWallpaper: TelegramWallpaper
|
||||||
|
var peer: EnginePeer
|
||||||
|
var constrainedSize: CGSize
|
||||||
|
|
||||||
|
init(theme: PresentationTheme, strings: PresentationStrings, chatWallpaper: TelegramWallpaper, peer: EnginePeer, constrainedSize: CGSize) {
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.chatWallpaper = chatWallpaper
|
||||||
|
self.peer = peer
|
||||||
|
self.constrainedSize = constrainedSize
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Params, rhs: Params) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.chatWallpaper != rhs.chatWallpaper {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.constrainedSize != rhs.constrainedSize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Layout {
|
||||||
|
var params: Params
|
||||||
|
var size: CGSize
|
||||||
|
|
||||||
|
init(params: Params, size: CGSize) {
|
||||||
|
self.params = params
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let textNode: ImmediateTextNode
|
||||||
|
private var backgroundContent: WallpaperBubbleBackgroundNode?
|
||||||
|
private let textMaskNode: LinkHighlightingNode
|
||||||
|
|
||||||
|
private let badgeTextNode: ImmediateTextNode
|
||||||
|
private let badgeBackgroundView: UIImageView
|
||||||
|
|
||||||
|
private var currentLayout: Layout?
|
||||||
|
|
||||||
|
var action: (() -> Void)?
|
||||||
|
|
||||||
|
override init(pointerStyle: PointerStyle? = nil) {
|
||||||
|
self.textNode = ImmediateTextNode()
|
||||||
|
self.textNode.textAlignment = .center
|
||||||
|
self.textNode.maximumNumberOfLines = 0
|
||||||
|
|
||||||
|
self.textMaskNode = LinkHighlightingNode(color: .white)
|
||||||
|
self.textMaskNode.innerRadius = 5.0
|
||||||
|
self.textMaskNode.outerRadius = 10.0
|
||||||
|
self.textMaskNode.inset = 0.0
|
||||||
|
|
||||||
|
self.badgeTextNode = ImmediateTextNode()
|
||||||
|
self.badgeBackgroundView = UIImageView()
|
||||||
|
|
||||||
|
super.init(pointerStyle: pointerStyle)
|
||||||
|
|
||||||
|
self.addSubnode(self.textNode)
|
||||||
|
|
||||||
|
self.view.addSubview(self.badgeBackgroundView)
|
||||||
|
self.addSubnode(self.badgeTextNode)
|
||||||
|
|
||||||
|
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
|
self.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if let self, self.bounds.width > 0.0 {
|
||||||
|
let animateScale = true
|
||||||
|
|
||||||
|
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
|
||||||
|
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
|
||||||
|
|
||||||
|
if highlighted {
|
||||||
|
self.layer.removeAnimation(forKey: "transform.scale")
|
||||||
|
|
||||||
|
if animateScale {
|
||||||
|
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||||
|
transition.setScale(layer: self.layer, scale: topScale)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if animateScale {
|
||||||
|
let transition = Transition(animation: .none)
|
||||||
|
transition.setScale(layer: self.layer, scale: 1.0)
|
||||||
|
|
||||||
|
self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
self.action?()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
chatWallpaper: TelegramWallpaper,
|
||||||
|
peer: EnginePeer,
|
||||||
|
wallpaperBackgroundNode: WallpaperBackgroundNode?,
|
||||||
|
constrainedSize: CGSize
|
||||||
|
) -> CGSize {
|
||||||
|
let params = Params(
|
||||||
|
theme: theme,
|
||||||
|
strings: strings,
|
||||||
|
chatWallpaper: chatWallpaper,
|
||||||
|
peer: peer,
|
||||||
|
constrainedSize: constrainedSize
|
||||||
|
)
|
||||||
|
if let currentLayout = self.currentLayout, currentLayout.params == params {
|
||||||
|
return currentLayout.size
|
||||||
|
} else {
|
||||||
|
let size = self.updateInternal(params: params, wallpaperBackgroundNode: wallpaperBackgroundNode)
|
||||||
|
self.currentLayout = Layout(params: params, size: size)
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateInternal(params: Params, wallpaperBackgroundNode: WallpaperBackgroundNode?) -> CGSize {
|
||||||
|
let serviceColor = serviceMessageColorComponents(theme: params.theme, wallpaper: params.chatWallpaper)
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
let textString = NSMutableAttributedString()
|
||||||
|
textString.append(NSAttributedString(string: "\(params.peer.compactDisplayTitle) added the message above for all empty chats", font: Font.regular(13.0), textColor: serviceColor.primaryText))
|
||||||
|
textString.append(NSAttributedString(string: " .how?", font: Font.regular(11.0), textColor: .clear))
|
||||||
|
self.textNode.attributedText = textString
|
||||||
|
|
||||||
|
let maxTextSize = CGSize(width: min(300.0, params.constrainedSize.width - 8.0 * 2.0), height: params.constrainedSize.height - 8.0 * 2.0)
|
||||||
|
|
||||||
|
var bestSize: (availableWidth: CGFloat, info: TextNodeLayout)
|
||||||
|
let info = self.textNode.updateLayoutFullInfo(maxTextSize)
|
||||||
|
bestSize = (maxTextSize.width, info)
|
||||||
|
if info.numberOfLines > 1 {
|
||||||
|
let measureIncrement = 8.0
|
||||||
|
var measureWidth = info.size.width
|
||||||
|
measureWidth -= measureIncrement
|
||||||
|
while measureWidth > 0.0 {
|
||||||
|
let otherInfo = self.textNode.updateLayoutFullInfo(CGSize(width: measureWidth, height: maxTextSize.height))
|
||||||
|
if otherInfo.numberOfLines > bestSize.info.numberOfLines {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (otherInfo.size.width - otherInfo.trailingLineWidth) < (bestSize.info.size.width - bestSize.info.trailingLineWidth) {
|
||||||
|
bestSize = (measureWidth, otherInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
measureWidth -= measureIncrement
|
||||||
|
}
|
||||||
|
|
||||||
|
let bestInfo = self.textNode.updateLayoutFullInfo(CGSize(width: bestSize.availableWidth, height: maxTextSize.height))
|
||||||
|
bestSize = (maxTextSize.width, bestInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
let textLayout = bestSize.info
|
||||||
|
|
||||||
|
var labelRects = textLayout.linesRects()
|
||||||
|
if labelRects.count > 1 {
|
||||||
|
let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width })
|
||||||
|
for i in 0 ..< sortedIndices.count {
|
||||||
|
let index = sortedIndices[i]
|
||||||
|
for j in -1 ... 1 {
|
||||||
|
if j != 0 && index + j >= 0 && index + j < sortedIndices.count {
|
||||||
|
if abs(labelRects[index + j].width - labelRects[index].width) < 16.0 {
|
||||||
|
labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width)
|
||||||
|
labelRects[index].size.width = labelRects[index + j].size.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i in 0 ..< labelRects.count {
|
||||||
|
labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0))
|
||||||
|
labelRects[i].size.height = 20.0
|
||||||
|
labelRects[i].origin.x = floor((textLayout.size.width - labelRects[i].width) / 2.0)
|
||||||
|
}
|
||||||
|
self.textMaskNode.updateRects(labelRects)
|
||||||
|
|
||||||
|
let size = CGSize(width: textLayout.size.width + 4.0 * 2.0, height: textLayout.size.height + 4.0 * 2.0)
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: textLayout.size)
|
||||||
|
self.textNode.frame = textFrame
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
self.badgeTextNode.attributedText = NSAttributedString(string: "how?", font: Font.regular(11.0), textColor: serviceColor.primaryText)
|
||||||
|
let badgeTextSize = self.badgeTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
|
||||||
|
if let lastLineFrame = labelRects.last {
|
||||||
|
let badgeTextFrame = CGRect(origin: CGPoint(x: lastLineFrame.maxX - badgeTextSize.width - 2.0, y: textFrame.maxY - badgeTextSize.height), size: badgeTextSize)
|
||||||
|
self.badgeTextNode.frame = badgeTextFrame
|
||||||
|
|
||||||
|
let badgeBackgroundFrame = badgeTextFrame.insetBy(dx: -4.0, dy: -1.0)
|
||||||
|
if badgeBackgroundFrame.height != self.badgeBackgroundView.image?.size.height {
|
||||||
|
self.badgeBackgroundView.image = generateStretchableFilledCircleImage(diameter: badgeBackgroundFrame.height, color: serviceColor.primaryText.withMultipliedAlpha(0.1))
|
||||||
|
}
|
||||||
|
self.badgeBackgroundView.frame = badgeBackgroundFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
self.textMaskNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - self.textMaskNode.inset + 4.0, y: textFrame.minY - self.textMaskNode.inset - 11.0), size: CGSize())
|
||||||
|
|
||||||
|
if let wallpaperBackgroundNode {
|
||||||
|
if self.backgroundContent == nil, let backgroundContent = wallpaperBackgroundNode.makeBubbleBackground(for: .free) {
|
||||||
|
|
||||||
|
self.backgroundContent = backgroundContent
|
||||||
|
backgroundContent.view.mask = self.textMaskNode.view
|
||||||
|
self.insertSubnode(backgroundContent, at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let backgroundContent = self.backgroundContent {
|
||||||
|
backgroundContent.frame = CGRect(origin: CGPoint(x: -4.0, y: 0.0), size: CGSize(width: size.width + 4.0 * 2.0, height: size.height))
|
||||||
|
}
|
||||||
|
} else if let backgroundContent = self.backgroundContent {
|
||||||
|
self.backgroundContent = nil
|
||||||
|
backgroundContent.removeFromSupernode()
|
||||||
|
}
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAbsolutePosition(rect: CGRect, containerSize: CGSize, transition: ContainedViewLayoutTransition) {
|
||||||
|
guard let backgroundContent = self.backgroundContent else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var backgroundFrame = backgroundContent.frame
|
||||||
|
backgroundFrame.origin.x += rect.minX
|
||||||
|
backgroundFrame.origin.y += rect.minY
|
||||||
|
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class ChatEmptyNode: ASDisplayNode {
|
public final class ChatEmptyNode: ASDisplayNode {
|
||||||
public enum Subject {
|
public enum Subject {
|
||||||
public enum EmptyType: Equatable {
|
public enum EmptyType: Equatable {
|
||||||
@ -1203,6 +1447,7 @@ public final class ChatEmptyNode: ASDisplayNode {
|
|||||||
private var currentStrings: PresentationStrings?
|
private var currentStrings: PresentationStrings?
|
||||||
|
|
||||||
private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)?
|
private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)?
|
||||||
|
private var attachedDescriptionNode: EmptyAttachedDescriptionNode?
|
||||||
|
|
||||||
public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
|
public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -1247,6 +1492,12 @@ public final class ChatEmptyNode: ASDisplayNode {
|
|||||||
backgroundContent.cornerRadius = initialFrame.size.width / 2.0
|
backgroundContent.cornerRadius = initialFrame.size.width / 2.0
|
||||||
transition.updateCornerRadius(layer: backgroundContent.layer, cornerRadius: targetCornerRadius)
|
transition.updateCornerRadius(layer: backgroundContent.layer, cornerRadius: targetCornerRadius)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let attachedDescriptionNode = self.attachedDescriptionNode {
|
||||||
|
attachedDescriptionNode.layer.animatePosition(from: initialFrame.center, to: attachedDescriptionNode.position, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
attachedDescriptionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
|
attachedDescriptionNode.layer.animateScale(from: 0.001, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||||
@ -1265,6 +1516,7 @@ public final class ChatEmptyNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let contentType: ChatEmptyNodeContentType
|
let contentType: ChatEmptyNodeContentType
|
||||||
|
var displayAttachedDescription = false
|
||||||
switch subject {
|
switch subject {
|
||||||
case .detailsPlaceholder:
|
case .detailsPlaceholder:
|
||||||
contentType = .regular
|
contentType = .regular
|
||||||
@ -1298,6 +1550,9 @@ public final class ChatEmptyNode: ASDisplayNode {
|
|||||||
contentType = .regular
|
contentType = .regular
|
||||||
} else {
|
} else {
|
||||||
contentType = .greeting
|
contentType = .greeting
|
||||||
|
if interfaceState.businessIntro != nil {
|
||||||
|
displayAttachedDescription = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1368,6 +1623,46 @@ public final class ChatEmptyNode: ASDisplayNode {
|
|||||||
transition.updateFrame(node: self.backgroundNode, frame: contentFrame)
|
transition.updateFrame(node: self.backgroundNode, frame: contentFrame)
|
||||||
self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(20.0, self.backgroundNode.bounds.height / 2.0), transition: transition)
|
self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(20.0, self.backgroundNode.bounds.height / 2.0), transition: transition)
|
||||||
|
|
||||||
|
if displayAttachedDescription, let peer = interfaceState.renderedPeer?.chatMainPeer {
|
||||||
|
let attachedDescriptionNode: EmptyAttachedDescriptionNode
|
||||||
|
if let current = self.attachedDescriptionNode {
|
||||||
|
attachedDescriptionNode = current
|
||||||
|
} else {
|
||||||
|
attachedDescriptionNode = EmptyAttachedDescriptionNode()
|
||||||
|
self.attachedDescriptionNode = attachedDescriptionNode
|
||||||
|
self.addSubnode(attachedDescriptionNode)
|
||||||
|
|
||||||
|
attachedDescriptionNode.action = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .settings, forceDark: false, dismissed: nil)
|
||||||
|
self.interaction?.chatController()?.push(controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachedDescriptionSize = attachedDescriptionNode.update(
|
||||||
|
theme: interfaceState.theme,
|
||||||
|
strings: interfaceState.strings,
|
||||||
|
chatWallpaper: interfaceState.chatWallpaper,
|
||||||
|
peer: EnginePeer(peer),
|
||||||
|
wallpaperBackgroundNode: backgroundNode,
|
||||||
|
constrainedSize: CGSize(width: size.width - insets.left - insets.right, height: 200.0)
|
||||||
|
)
|
||||||
|
let attachedDescriptionFrame = CGRect(origin: CGPoint(x: floor((size.width - attachedDescriptionSize.width) * 0.5), y: contentFrame.maxY + 4.0), size: attachedDescriptionSize)
|
||||||
|
transition.updateFrame(node: attachedDescriptionNode, frame: attachedDescriptionFrame)
|
||||||
|
|
||||||
|
if let (rect, containerSize) = self.absolutePosition {
|
||||||
|
var backgroundFrame = attachedDescriptionNode.frame
|
||||||
|
backgroundFrame.origin.x += rect.minX
|
||||||
|
backgroundFrame.origin.y += rect.minY
|
||||||
|
attachedDescriptionNode.updateAbsolutePosition(rect: backgroundFrame, containerSize: containerSize, transition: .immediate)
|
||||||
|
}
|
||||||
|
} else if let attachedDescriptionNode = self.attachedDescriptionNode {
|
||||||
|
self.attachedDescriptionNode = nil
|
||||||
|
attachedDescriptionNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
|
||||||
if backgroundNode?.hasExtraBubbleBackground() == true {
|
if backgroundNode?.hasExtraBubbleBackground() == true {
|
||||||
if self.backgroundContent == nil, let backgroundContent = backgroundNode?.makeBubbleBackground(for: .free) {
|
if self.backgroundContent == nil, let backgroundContent = backgroundNode?.makeBubbleBackground(for: .free) {
|
||||||
backgroundContent.clipsToBounds = true
|
backgroundContent.clipsToBounds = true
|
||||||
@ -1409,5 +1704,12 @@ public final class ChatEmptyNode: ASDisplayNode {
|
|||||||
backgroundFrame.origin.y += rect.minY
|
backgroundFrame.origin.y += rect.minY
|
||||||
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition)
|
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let attachedDescriptionNode = self.attachedDescriptionNode {
|
||||||
|
var backgroundFrame = attachedDescriptionNode.frame
|
||||||
|
backgroundFrame.origin.x += rect.minX
|
||||||
|
backgroundFrame.origin.y += rect.minY
|
||||||
|
attachedDescriptionNode.updateAbsolutePosition(rect: backgroundFrame, containerSize: containerSize, transition: transition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,10 +91,11 @@ private final class SendInviteLinkScreenComponent: Component {
|
|||||||
private var premiumSeparatorRight: SimpleLayer?
|
private var premiumSeparatorRight: SimpleLayer?
|
||||||
private var premiumSeparatorText: ComponentView<Empty>?
|
private var premiumSeparatorText: ComponentView<Empty>?
|
||||||
|
|
||||||
private let title = ComponentView<Empty>()
|
|
||||||
private let leftButton = ComponentView<Empty>()
|
private let leftButton = ComponentView<Empty>()
|
||||||
private let descriptionText = ComponentView<Empty>()
|
|
||||||
private let actionButton = ComponentView<Empty>()
|
private var title: ComponentView<Empty>?
|
||||||
|
private var descriptionText: ComponentView<Empty>?
|
||||||
|
private var actionButton: ComponentView<Empty>?
|
||||||
|
|
||||||
private let itemContainerView: UIView
|
private let itemContainerView: UIView
|
||||||
private var items: [AnyHashable: ComponentView<Empty>] = [:]
|
private var items: [AnyHashable: ComponentView<Empty>] = [:]
|
||||||
@ -242,7 +243,7 @@ private final class SendInviteLinkScreenComponent: Component {
|
|||||||
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
if let actionButtonView = self.actionButton.view {
|
if let actionButtonView = self.actionButton?.view {
|
||||||
actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,7 +257,7 @@ private final class SendInviteLinkScreenComponent: Component {
|
|||||||
})
|
})
|
||||||
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
||||||
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
||||||
if let actionButtonView = self.actionButton.view {
|
if let actionButtonView = self.actionButton?.view {
|
||||||
actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,6 +281,7 @@ private final class SendInviteLinkScreenComponent: Component {
|
|||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
|
||||||
let hasPremiumRestrictedUsers = "".isEmpty
|
let hasPremiumRestrictedUsers = "".isEmpty
|
||||||
|
let hasInviteLink = "".isEmpty
|
||||||
|
|
||||||
if themeUpdated {
|
if themeUpdated {
|
||||||
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||||
@ -373,35 +375,6 @@ private final class SendInviteLinkScreenComponent: Component {
|
|||||||
self.premiumButton = premiumButton
|
self.premiumButton = premiumButton
|
||||||
}
|
}
|
||||||
|
|
||||||
let premiumSeparatorText: ComponentView<Empty>
|
|
||||||
if let current = self.premiumSeparatorText {
|
|
||||||
premiumSeparatorText = current
|
|
||||||
} else {
|
|
||||||
premiumSeparatorText = ComponentView()
|
|
||||||
self.premiumSeparatorText = premiumSeparatorText
|
|
||||||
}
|
|
||||||
|
|
||||||
let premiumSeparatorLeft: SimpleLayer
|
|
||||||
if let current = self.premiumSeparatorLeft {
|
|
||||||
premiumSeparatorLeft = current
|
|
||||||
} else {
|
|
||||||
premiumSeparatorLeft = SimpleLayer()
|
|
||||||
self.premiumSeparatorLeft = premiumSeparatorLeft
|
|
||||||
self.scrollContentView.layer.addSublayer(premiumSeparatorLeft)
|
|
||||||
}
|
|
||||||
|
|
||||||
let premiumSeparatorRight: SimpleLayer
|
|
||||||
if let current = self.premiumSeparatorRight {
|
|
||||||
premiumSeparatorRight = current
|
|
||||||
} else {
|
|
||||||
premiumSeparatorRight = SimpleLayer()
|
|
||||||
self.premiumSeparatorRight = premiumSeparatorRight
|
|
||||||
self.scrollContentView.layer.addSublayer(premiumSeparatorRight)
|
|
||||||
}
|
|
||||||
|
|
||||||
premiumSeparatorLeft.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
|
|
||||||
premiumSeparatorRight.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
|
|
||||||
|
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
let premiumTitleSize = premiumTitle.update(
|
let premiumTitleSize = premiumTitle.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
@ -531,31 +504,78 @@ private final class SendInviteLinkScreenComponent: Component {
|
|||||||
transition.setFrame(view: premiumButtonView, frame: premiumButtonFrame)
|
transition.setFrame(view: premiumButtonView, frame: premiumButtonFrame)
|
||||||
}
|
}
|
||||||
contentHeight += premiumButtonSize.height
|
contentHeight += premiumButtonSize.height
|
||||||
contentHeight += 19.0
|
|
||||||
|
|
||||||
let premiumSeparatorTextSize = premiumSeparatorText.update(
|
if hasInviteLink {
|
||||||
transition: .immediate,
|
let premiumSeparatorText: ComponentView<Empty>
|
||||||
component: AnyComponent(MultilineTextComponent(
|
if let current = self.premiumSeparatorText {
|
||||||
text: .plain(NSAttributedString(string: "or", font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor))
|
premiumSeparatorText = current
|
||||||
)),
|
} else {
|
||||||
environment: {},
|
premiumSeparatorText = ComponentView()
|
||||||
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
|
self.premiumSeparatorText = premiumSeparatorText
|
||||||
)
|
|
||||||
let premiumSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumSeparatorTextSize.width) * 0.5), y: contentHeight), size: premiumSeparatorTextSize)
|
|
||||||
if let premiumSeparatorTextView = premiumSeparatorText.view {
|
|
||||||
if premiumSeparatorTextView.superview == nil {
|
|
||||||
self.scrollContentView.addSubview(premiumSeparatorTextView)
|
|
||||||
}
|
}
|
||||||
transition.setFrame(view: premiumSeparatorTextView, frame: premiumSeparatorTextFrame)
|
|
||||||
|
let premiumSeparatorLeft: SimpleLayer
|
||||||
|
if let current = self.premiumSeparatorLeft {
|
||||||
|
premiumSeparatorLeft = current
|
||||||
|
} else {
|
||||||
|
premiumSeparatorLeft = SimpleLayer()
|
||||||
|
self.premiumSeparatorLeft = premiumSeparatorLeft
|
||||||
|
self.scrollContentView.layer.addSublayer(premiumSeparatorLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
let premiumSeparatorRight: SimpleLayer
|
||||||
|
if let current = self.premiumSeparatorRight {
|
||||||
|
premiumSeparatorRight = current
|
||||||
|
} else {
|
||||||
|
premiumSeparatorRight = SimpleLayer()
|
||||||
|
self.premiumSeparatorRight = premiumSeparatorRight
|
||||||
|
self.scrollContentView.layer.addSublayer(premiumSeparatorRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
premiumSeparatorLeft.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
|
||||||
|
premiumSeparatorRight.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
|
||||||
|
|
||||||
|
contentHeight += 19.0
|
||||||
|
|
||||||
|
let premiumSeparatorTextSize = premiumSeparatorText.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: "or", font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
|
||||||
|
)
|
||||||
|
let premiumSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumSeparatorTextSize.width) * 0.5), y: contentHeight), size: premiumSeparatorTextSize)
|
||||||
|
if let premiumSeparatorTextView = premiumSeparatorText.view {
|
||||||
|
if premiumSeparatorTextView.superview == nil {
|
||||||
|
self.scrollContentView.addSubview(premiumSeparatorTextView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: premiumSeparatorTextView, frame: premiumSeparatorTextFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
let separatorWidth: CGFloat = 72.0
|
||||||
|
let separatorSpacing: CGFloat = 10.0
|
||||||
|
|
||||||
|
transition.setFrame(layer: premiumSeparatorLeft, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
|
||||||
|
transition.setFrame(layer: premiumSeparatorRight, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.maxX + separatorSpacing, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
|
||||||
|
|
||||||
|
contentHeight += 31.0
|
||||||
|
} else {
|
||||||
|
if let premiumSeparatorLeft = self.premiumSeparatorLeft {
|
||||||
|
self.premiumSeparatorLeft = nil
|
||||||
|
premiumSeparatorLeft.removeFromSuperlayer()
|
||||||
|
}
|
||||||
|
if let premiumSeparatorRight = self.premiumSeparatorRight {
|
||||||
|
self.premiumSeparatorRight = nil
|
||||||
|
premiumSeparatorRight.removeFromSuperlayer()
|
||||||
|
}
|
||||||
|
if let premiumSeparatorText = self.premiumSeparatorText {
|
||||||
|
self.premiumSeparatorText = nil
|
||||||
|
premiumSeparatorText.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += 14.0
|
||||||
}
|
}
|
||||||
|
|
||||||
let separatorWidth: CGFloat = 72.0
|
|
||||||
let separatorSpacing: CGFloat = 10.0
|
|
||||||
|
|
||||||
transition.setFrame(layer: premiumSeparatorLeft, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
|
|
||||||
transition.setFrame(layer: premiumSeparatorRight, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.maxX + separatorSpacing, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
|
|
||||||
|
|
||||||
contentHeight += 31.0
|
|
||||||
} else {
|
} else {
|
||||||
if let premiumTitle = self.premiumTitle {
|
if let premiumTitle = self.premiumTitle {
|
||||||
self.premiumTitle = nil
|
self.premiumTitle = nil
|
||||||
@ -569,241 +589,278 @@ private final class SendInviteLinkScreenComponent: Component {
|
|||||||
self.premiumButton = nil
|
self.premiumButton = nil
|
||||||
premiumButton.view?.removeFromSuperview()
|
premiumButton.view?.removeFromSuperview()
|
||||||
}
|
}
|
||||||
if let premiumSeparatorLeft = self.premiumSeparatorLeft {
|
|
||||||
self.premiumSeparatorLeft = nil
|
|
||||||
premiumSeparatorLeft.removeFromSuperlayer()
|
|
||||||
}
|
|
||||||
if let premiumSeparatorRight = self.premiumSeparatorRight {
|
|
||||||
self.premiumSeparatorRight = nil
|
|
||||||
premiumSeparatorRight.removeFromSuperlayer()
|
|
||||||
}
|
|
||||||
if let premiumSeparatorText = self.premiumSeparatorText {
|
|
||||||
self.premiumSeparatorText = nil
|
|
||||||
premiumSeparatorText.view?.removeFromSuperview()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleSize = self.title.update(
|
|
||||||
transition: .immediate,
|
|
||||||
component: AnyComponent(MultilineTextComponent(
|
|
||||||
text: .plain(NSAttributedString(string: component.link != nil ? environment.strings.SendInviteLink_InviteTitle : environment.strings.SendInviteLink_LinkUnavailableTitle, font: Font.semibold(24.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
|
||||||
)),
|
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
|
|
||||||
)
|
|
||||||
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)
|
|
||||||
if let titleView = self.title.view {
|
|
||||||
if titleView.superview == nil {
|
|
||||||
self.scrollContentView.addSubview(titleView)
|
|
||||||
}
|
|
||||||
transition.setFrame(view: titleView, frame: titleFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentHeight += titleSize.height
|
|
||||||
contentHeight += 8.0
|
|
||||||
|
|
||||||
let text: String
|
|
||||||
if hasPremiumRestrictedUsers {
|
|
||||||
if component.link != nil {
|
|
||||||
//TODO:localize
|
|
||||||
text = "You can try to send an invite link instead."
|
|
||||||
} else {
|
|
||||||
if component.peers.count == 1 {
|
|
||||||
text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string
|
|
||||||
} else {
|
|
||||||
text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if component.link != nil {
|
|
||||||
if component.peers.count == 1 {
|
|
||||||
text = environment.strings.SendInviteLink_TextAvailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string
|
|
||||||
} else {
|
|
||||||
text = environment.strings.SendInviteLink_TextAvailableMultipleUsers(Int32(component.peers.count))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if component.peers.count == 1 {
|
|
||||||
text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string
|
|
||||||
} else {
|
|
||||||
text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
|
|
||||||
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
|
|
||||||
|
|
||||||
let descriptionTextSize = self.descriptionText.update(
|
|
||||||
transition: .immediate,
|
|
||||||
component: AnyComponent(MultilineTextComponent(
|
|
||||||
text: .markdown(text: text, attributes: MarkdownAttributes(
|
|
||||||
body: body,
|
|
||||||
bold: bold,
|
|
||||||
link: body,
|
|
||||||
linkAttribute: { _ in nil }
|
|
||||||
)),
|
|
||||||
horizontalAlignment: .center,
|
|
||||||
maximumNumberOfLines: 0
|
|
||||||
)),
|
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0)
|
|
||||||
)
|
|
||||||
let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize)
|
|
||||||
if let descriptionTextView = self.descriptionText.view {
|
|
||||||
if descriptionTextView.superview == nil {
|
|
||||||
self.scrollContentView.addSubview(descriptionTextView)
|
|
||||||
}
|
|
||||||
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentHeight += descriptionTextFrame.height
|
|
||||||
contentHeight += 22.0
|
|
||||||
|
|
||||||
var singleItemHeight: CGFloat = 0.0
|
|
||||||
|
|
||||||
var itemsHeight: CGFloat = 0.0
|
|
||||||
var validIds: [AnyHashable] = []
|
|
||||||
for i in 0 ..< component.peers.count {
|
|
||||||
let peer = component.peers[i]
|
|
||||||
|
|
||||||
for _ in 0 ..< 1 {
|
|
||||||
//let id: AnyHashable = AnyHashable("\(peer.id)_\(j)")
|
|
||||||
let id = AnyHashable(peer.id)
|
|
||||||
validIds.append(id)
|
|
||||||
|
|
||||||
let item: ComponentView<Empty>
|
|
||||||
var itemTransition = transition
|
|
||||||
if let current = self.items[id] {
|
|
||||||
item = current
|
|
||||||
} else {
|
|
||||||
itemTransition = .immediate
|
|
||||||
item = ComponentView()
|
|
||||||
self.items[id] = item
|
|
||||||
}
|
|
||||||
|
|
||||||
let itemSize = item.update(
|
|
||||||
transition: itemTransition,
|
|
||||||
component: AnyComponent(PeerListItemComponent(
|
|
||||||
context: component.context,
|
|
||||||
theme: environment.theme,
|
|
||||||
strings: environment.strings,
|
|
||||||
sideInset: 0.0,
|
|
||||||
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
|
||||||
peer: peer,
|
|
||||||
presence: component.peerPresences[peer.id],
|
|
||||||
selectionState: component.link == nil ? .none : .editing(isSelected: self.selectedItems.contains(peer.id)),
|
|
||||||
hasNext: i != component.peers.count - 1,
|
|
||||||
action: { [weak self] peer in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if self.selectedItems.contains(peer.id) {
|
|
||||||
self.selectedItems.remove(peer.id)
|
|
||||||
} else {
|
|
||||||
self.selectedItems.insert(peer.id)
|
|
||||||
}
|
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
|
||||||
}
|
|
||||||
)),
|
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
|
||||||
)
|
|
||||||
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
|
|
||||||
|
|
||||||
if let itemView = item.view {
|
|
||||||
if itemView.superview == nil {
|
|
||||||
self.itemContainerView.addSubview(itemView)
|
|
||||||
}
|
|
||||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsHeight += itemSize.height
|
|
||||||
singleItemHeight = itemSize.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var removeIds: [AnyHashable] = []
|
|
||||||
for (id, item) in self.items {
|
|
||||||
if !validIds.contains(id) {
|
|
||||||
removeIds.append(id)
|
|
||||||
item.view?.removeFromSuperview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for id in removeIds {
|
|
||||||
self.items.removeValue(forKey: id)
|
|
||||||
}
|
|
||||||
transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight)))
|
|
||||||
|
|
||||||
var initialContentHeight = contentHeight
|
|
||||||
initialContentHeight += min(itemsHeight, floor(singleItemHeight * 2.5))
|
|
||||||
|
|
||||||
contentHeight += itemsHeight
|
|
||||||
contentHeight += 24.0
|
|
||||||
initialContentHeight += 24.0
|
|
||||||
|
|
||||||
let actionButtonTitle: String
|
|
||||||
if component.link != nil {
|
|
||||||
actionButtonTitle = self.selectedItems.isEmpty ? environment.strings.SendInviteLink_ActionSkip : environment.strings.SendInviteLink_ActionInvite
|
|
||||||
} else {
|
|
||||||
actionButtonTitle = environment.strings.SendInviteLink_ActionClose
|
|
||||||
}
|
|
||||||
let actionButtonSize = self.actionButton.update(
|
|
||||||
transition: transition,
|
|
||||||
component: AnyComponent(SolidRoundedButtonComponent(
|
|
||||||
title: actionButtonTitle,
|
|
||||||
badge: (self.selectedItems.isEmpty || component.link == nil) ? nil : "\(self.selectedItems.count)",
|
|
||||||
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
|
|
||||||
font: .bold,
|
|
||||||
fontSize: 17.0,
|
|
||||||
height: 50.0,
|
|
||||||
cornerRadius: 11.0,
|
|
||||||
gloss: false,
|
|
||||||
animationName: nil,
|
|
||||||
iconPosition: .right,
|
|
||||||
iconSpacing: 4.0,
|
|
||||||
action: { [weak self] in
|
|
||||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if self.selectedItems.isEmpty {
|
|
||||||
controller.dismiss()
|
|
||||||
} else if let link = component.link {
|
|
||||||
let selectedPeers = component.peers.filter { self.selectedItems.contains($0.id) }
|
|
||||||
|
|
||||||
let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start()
|
|
||||||
let text: String
|
|
||||||
if selectedPeers.count == 1 {
|
|
||||||
text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string
|
|
||||||
} else if selectedPeers.count == 2 {
|
|
||||||
text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string
|
|
||||||
} else {
|
|
||||||
text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string
|
|
||||||
}
|
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root))
|
|
||||||
|
|
||||||
controller.dismiss()
|
|
||||||
} else {
|
|
||||||
controller.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)),
|
|
||||||
environment: {},
|
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
|
||||||
)
|
|
||||||
let bottomPanelHeight = 15.0 + environment.safeInsets.bottom + actionButtonSize.height
|
|
||||||
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
|
||||||
if let actionButtonView = self.actionButton.view {
|
|
||||||
if actionButtonView.superview == nil {
|
|
||||||
self.addSubview(actionButtonView)
|
|
||||||
}
|
|
||||||
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentHeight += bottomPanelHeight
|
|
||||||
initialContentHeight += bottomPanelHeight
|
|
||||||
|
|
||||||
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
||||||
|
|
||||||
|
var initialContentHeight = contentHeight
|
||||||
|
let clippingY: CGFloat
|
||||||
|
|
||||||
|
if hasInviteLink {
|
||||||
|
let title: ComponentView<Empty>
|
||||||
|
if let current = self.title {
|
||||||
|
title = current
|
||||||
|
} else {
|
||||||
|
title = ComponentView()
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
let descriptionText: ComponentView<Empty>
|
||||||
|
if let current = self.descriptionText {
|
||||||
|
descriptionText = current
|
||||||
|
} else {
|
||||||
|
descriptionText = ComponentView()
|
||||||
|
self.descriptionText = descriptionText
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionButton: ComponentView<Empty>
|
||||||
|
if let current = self.actionButton {
|
||||||
|
actionButton = current
|
||||||
|
} else {
|
||||||
|
actionButton = ComponentView()
|
||||||
|
self.actionButton = actionButton
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleSize = title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: component.link != nil ? environment.strings.SendInviteLink_InviteTitle : environment.strings.SendInviteLink_LinkUnavailableTitle, font: Font.semibold(24.0), textColor: environment.theme.list.itemPrimaryTextColor))
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
|
||||||
|
)
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)
|
||||||
|
if let titleView = title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
self.scrollContentView.addSubview(titleView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: titleView, frame: titleFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += titleSize.height
|
||||||
|
contentHeight += 8.0
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
if hasPremiumRestrictedUsers {
|
||||||
|
if component.link != nil {
|
||||||
|
//TODO:localize
|
||||||
|
text = "You can try to send an invite link instead."
|
||||||
|
} else {
|
||||||
|
if component.peers.count == 1 {
|
||||||
|
text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string
|
||||||
|
} else {
|
||||||
|
text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if component.link != nil {
|
||||||
|
if component.peers.count == 1 {
|
||||||
|
text = environment.strings.SendInviteLink_TextAvailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string
|
||||||
|
} else {
|
||||||
|
text = environment.strings.SendInviteLink_TextAvailableMultipleUsers(Int32(component.peers.count))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if component.peers.count == 1 {
|
||||||
|
text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string
|
||||||
|
} else {
|
||||||
|
text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
|
||||||
|
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
|
||||||
|
|
||||||
|
let descriptionTextSize = descriptionText.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .markdown(text: text, attributes: MarkdownAttributes(
|
||||||
|
body: body,
|
||||||
|
bold: bold,
|
||||||
|
link: body,
|
||||||
|
linkAttribute: { _ in nil }
|
||||||
|
)),
|
||||||
|
horizontalAlignment: .center,
|
||||||
|
maximumNumberOfLines: 0
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0)
|
||||||
|
)
|
||||||
|
let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize)
|
||||||
|
if let descriptionTextView = descriptionText.view {
|
||||||
|
if descriptionTextView.superview == nil {
|
||||||
|
self.scrollContentView.addSubview(descriptionTextView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += descriptionTextFrame.height
|
||||||
|
contentHeight += 22.0
|
||||||
|
|
||||||
|
var singleItemHeight: CGFloat = 0.0
|
||||||
|
|
||||||
|
var itemsHeight: CGFloat = 0.0
|
||||||
|
var validIds: [AnyHashable] = []
|
||||||
|
for i in 0 ..< component.peers.count {
|
||||||
|
let peer = component.peers[i]
|
||||||
|
|
||||||
|
for _ in 0 ..< 1 {
|
||||||
|
//let id: AnyHashable = AnyHashable("\(peer.id)_\(j)")
|
||||||
|
let id = AnyHashable(peer.id)
|
||||||
|
validIds.append(id)
|
||||||
|
|
||||||
|
let item: ComponentView<Empty>
|
||||||
|
var itemTransition = transition
|
||||||
|
if let current = self.items[id] {
|
||||||
|
item = current
|
||||||
|
} else {
|
||||||
|
itemTransition = .immediate
|
||||||
|
item = ComponentView()
|
||||||
|
self.items[id] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemSize = item.update(
|
||||||
|
transition: itemTransition,
|
||||||
|
component: AnyComponent(PeerListItemComponent(
|
||||||
|
context: component.context,
|
||||||
|
theme: environment.theme,
|
||||||
|
strings: environment.strings,
|
||||||
|
sideInset: 0.0,
|
||||||
|
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
|
peer: peer,
|
||||||
|
presence: component.peerPresences[peer.id],
|
||||||
|
selectionState: component.link == nil ? .none : .editing(isSelected: self.selectedItems.contains(peer.id)),
|
||||||
|
hasNext: i != component.peers.count - 1,
|
||||||
|
action: { [weak self] peer in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.selectedItems.contains(peer.id) {
|
||||||
|
self.selectedItems.remove(peer.id)
|
||||||
|
} else {
|
||||||
|
self.selectedItems.insert(peer.id)
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
|
||||||
|
)
|
||||||
|
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
|
||||||
|
|
||||||
|
if let itemView = item.view {
|
||||||
|
if itemView.superview == nil {
|
||||||
|
self.itemContainerView.addSubview(itemView)
|
||||||
|
}
|
||||||
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsHeight += itemSize.height
|
||||||
|
singleItemHeight = itemSize.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var removeIds: [AnyHashable] = []
|
||||||
|
for (id, item) in self.items {
|
||||||
|
if !validIds.contains(id) {
|
||||||
|
removeIds.append(id)
|
||||||
|
item.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in removeIds {
|
||||||
|
self.items.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight)))
|
||||||
|
|
||||||
|
initialContentHeight += singleItemHeight + 16.0
|
||||||
|
initialContentHeight += min(itemsHeight, floor(singleItemHeight * 2.5))
|
||||||
|
|
||||||
|
contentHeight += itemsHeight
|
||||||
|
contentHeight += 24.0
|
||||||
|
initialContentHeight += 24.0
|
||||||
|
|
||||||
|
let actionButtonTitle: String
|
||||||
|
if component.link != nil {
|
||||||
|
actionButtonTitle = self.selectedItems.isEmpty ? environment.strings.SendInviteLink_ActionSkip : environment.strings.SendInviteLink_ActionInvite
|
||||||
|
} else {
|
||||||
|
actionButtonTitle = environment.strings.SendInviteLink_ActionClose
|
||||||
|
}
|
||||||
|
let actionButtonSize = actionButton.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(SolidRoundedButtonComponent(
|
||||||
|
title: actionButtonTitle,
|
||||||
|
badge: (self.selectedItems.isEmpty || component.link == nil) ? nil : "\(self.selectedItems.count)",
|
||||||
|
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
|
||||||
|
font: .bold,
|
||||||
|
fontSize: 17.0,
|
||||||
|
height: 50.0,
|
||||||
|
cornerRadius: 11.0,
|
||||||
|
gloss: false,
|
||||||
|
animationName: nil,
|
||||||
|
iconPosition: .right,
|
||||||
|
iconSpacing: 4.0,
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.selectedItems.isEmpty {
|
||||||
|
controller.dismiss()
|
||||||
|
} else if let link = component.link {
|
||||||
|
let selectedPeers = component.peers.filter { self.selectedItems.contains($0.id) }
|
||||||
|
|
||||||
|
let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start()
|
||||||
|
let text: String
|
||||||
|
if selectedPeers.count == 1 {
|
||||||
|
text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string
|
||||||
|
} else if selectedPeers.count == 2 {
|
||||||
|
text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string
|
||||||
|
} else {
|
||||||
|
text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root))
|
||||||
|
|
||||||
|
controller.dismiss()
|
||||||
|
} else {
|
||||||
|
controller.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
|
||||||
|
)
|
||||||
|
let bottomPanelHeight = 15.0 + environment.safeInsets.bottom + actionButtonSize.height
|
||||||
|
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
||||||
|
if let actionButtonView = actionButton.view {
|
||||||
|
if actionButtonView.superview == nil {
|
||||||
|
self.addSubview(actionButtonView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += bottomPanelHeight
|
||||||
|
initialContentHeight += bottomPanelHeight
|
||||||
|
|
||||||
|
clippingY = actionButtonFrame.minY - 24.0
|
||||||
|
} else {
|
||||||
|
if let title = self.title {
|
||||||
|
self.title = nil
|
||||||
|
title.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
if let descriptionText = self.descriptionText {
|
||||||
|
self.descriptionText = nil
|
||||||
|
descriptionText.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
if let actionButton = self.actionButton {
|
||||||
|
self.actionButton = nil
|
||||||
|
actionButton.view?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
initialContentHeight += environment.safeInsets.bottom
|
||||||
|
|
||||||
|
clippingY = availableSize.height
|
||||||
|
}
|
||||||
|
|
||||||
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
|
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
|
||||||
|
|
||||||
let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset)
|
let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset)
|
||||||
@ -817,12 +874,12 @@ private final class SendInviteLinkScreenComponent: Component {
|
|||||||
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
||||||
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
|
|
||||||
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 24.0 - (containerInset)))
|
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset))
|
||||||
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
|
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
|
||||||
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
|
||||||
|
|
||||||
self.ignoreScrolling = true
|
self.ignoreScrolling = true
|
||||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height - containerInset)))
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
|
||||||
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
|
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
|
||||||
if contentSize != self.scrollView.contentSize {
|
if contentSize != self.scrollView.contentSize {
|
||||||
self.scrollView.contentSize = contentSize
|
self.scrollView.contentSize = contentSize
|
||||||
|
@ -1356,7 +1356,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
|||||||
sideInset: 0.0,
|
sideInset: 0.0,
|
||||||
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
peer: peer.peer,
|
peer: peer.peer,
|
||||||
subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContact,
|
subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser,
|
||||||
subtitleAccessory: .none,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
import MultilineTextComponent
|
||||||
|
import TelegramPresentationData
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
import ListSectionComponent
|
||||||
|
|
@ -120,6 +120,7 @@ final class ChatbotSetupScreenComponent: Component {
|
|||||||
private let nameSection = ComponentView<Empty>()
|
private let nameSection = ComponentView<Empty>()
|
||||||
private let accessSection = ComponentView<Empty>()
|
private let accessSection = ComponentView<Empty>()
|
||||||
private let excludedSection = ComponentView<Empty>()
|
private let excludedSection = ComponentView<Empty>()
|
||||||
|
private let excludedUsersSection = ComponentView<Empty>()
|
||||||
private let permissionsSection = ComponentView<Empty>()
|
private let permissionsSection = ComponentView<Empty>()
|
||||||
|
|
||||||
private var isUpdating: Bool = false
|
private var isUpdating: Bool = false
|
||||||
@ -299,7 +300,7 @@ final class ChatbotSetupScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openAdditionalPeerListSetup() {
|
private func openAdditionalPeerListSetup(isExclude: Bool) {
|
||||||
guard let component = self.component, let environment = self.environment else {
|
guard let component = self.component, let environment = self.environment else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -311,44 +312,51 @@ final class ChatbotSetupScreenComponent: Component {
|
|||||||
case nonContacts
|
case nonContacts
|
||||||
}
|
}
|
||||||
|
|
||||||
let additionalCategories: [ChatListNodeAdditionalCategory] = [
|
let additionalCategories: [ChatListNodeAdditionalCategory]
|
||||||
ChatListNodeAdditionalCategory(
|
|
||||||
id: self.hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.rawValue,
|
|
||||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), cornerRadius: 12.0, color: .purple),
|
|
||||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .purple),
|
|
||||||
title: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats : environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats
|
|
||||||
),
|
|
||||||
ChatListNodeAdditionalCategory(
|
|
||||||
id: AdditionalCategoryId.contacts.rawValue,
|
|
||||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue),
|
|
||||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
|
|
||||||
title: environment.strings.BusinessMessageSetup_Recipients_CategoryContacts
|
|
||||||
),
|
|
||||||
ChatListNodeAdditionalCategory(
|
|
||||||
id: AdditionalCategoryId.nonContacts.rawValue,
|
|
||||||
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow),
|
|
||||||
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow),
|
|
||||||
title: environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts
|
|
||||||
)
|
|
||||||
]
|
|
||||||
var selectedCategories = Set<Int>()
|
var selectedCategories = Set<Int>()
|
||||||
for category in self.additionalPeerList.categories {
|
if isExclude {
|
||||||
switch category {
|
additionalCategories = []
|
||||||
case .existingChats:
|
} else {
|
||||||
selectedCategories.insert(AdditionalCategoryId.existingChats.rawValue)
|
additionalCategories = [
|
||||||
case .newChats:
|
ChatListNodeAdditionalCategory(
|
||||||
selectedCategories.insert(AdditionalCategoryId.newChats.rawValue)
|
id: self.hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.rawValue,
|
||||||
case .contacts:
|
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), cornerRadius: 12.0, color: .purple),
|
||||||
selectedCategories.insert(AdditionalCategoryId.contacts.rawValue)
|
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .purple),
|
||||||
case .nonContacts:
|
title: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats : environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats
|
||||||
selectedCategories.insert(AdditionalCategoryId.nonContacts.rawValue)
|
),
|
||||||
|
ChatListNodeAdditionalCategory(
|
||||||
|
id: AdditionalCategoryId.contacts.rawValue,
|
||||||
|
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue),
|
||||||
|
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue),
|
||||||
|
title: environment.strings.BusinessMessageSetup_Recipients_CategoryContacts
|
||||||
|
),
|
||||||
|
ChatListNodeAdditionalCategory(
|
||||||
|
id: AdditionalCategoryId.nonContacts.rawValue,
|
||||||
|
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow),
|
||||||
|
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow),
|
||||||
|
title: environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if !isExclude {
|
||||||
|
for category in self.additionalPeerList.categories {
|
||||||
|
switch category {
|
||||||
|
case .existingChats:
|
||||||
|
selectedCategories.insert(AdditionalCategoryId.existingChats.rawValue)
|
||||||
|
case .newChats:
|
||||||
|
selectedCategories.insert(AdditionalCategoryId.newChats.rawValue)
|
||||||
|
case .contacts:
|
||||||
|
selectedCategories.insert(AdditionalCategoryId.contacts.rawValue)
|
||||||
|
case .nonContacts:
|
||||||
|
selectedCategories.insert(AdditionalCategoryId.nonContacts.rawValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
|
let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
|
||||||
title: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_ExcludeSearchTitle : environment.strings.BusinessMessageSetup_Recipients_IncludeSearchTitle,
|
title: (self.hasAccessToAllChatsByDefault || isExclude) ? environment.strings.BusinessMessageSetup_Recipients_ExcludeSearchTitle : environment.strings.BusinessMessageSetup_Recipients_IncludeSearchTitle,
|
||||||
searchPlaceholder: environment.strings.ChatListFilter_AddChatsSearchPlaceholder,
|
searchPlaceholder: environment.strings.ChatListFilter_AddChatsSearchPlaceholder,
|
||||||
selectedChats: Set(self.additionalPeerList.peers.map(\.peer.id)),
|
selectedChats: isExclude ? Set(self.additionalPeerList.excludePeers.map(\.peer.id)) : Set(self.additionalPeerList.peers.map(\.peer.id)),
|
||||||
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
|
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
|
||||||
chatListFilters: nil,
|
chatListFilters: nil,
|
||||||
onlyUsers: true
|
onlyUsers: true
|
||||||
@ -386,36 +394,59 @@ final class ChatbotSetupScreenComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let mappedCategories = additionalCategoryIds.compactMap { item -> AdditionalPeerList.Category? in
|
if !isExclude {
|
||||||
switch item {
|
let mappedCategories = additionalCategoryIds.compactMap { item -> AdditionalPeerList.Category? in
|
||||||
case AdditionalCategoryId.existingChats.rawValue:
|
switch item {
|
||||||
return .existingChats
|
case AdditionalCategoryId.existingChats.rawValue:
|
||||||
case AdditionalCategoryId.newChats.rawValue:
|
return .existingChats
|
||||||
return .newChats
|
case AdditionalCategoryId.newChats.rawValue:
|
||||||
case AdditionalCategoryId.contacts.rawValue:
|
return .newChats
|
||||||
return .contacts
|
case AdditionalCategoryId.contacts.rawValue:
|
||||||
case AdditionalCategoryId.nonContacts.rawValue:
|
return .contacts
|
||||||
return .nonContacts
|
case AdditionalCategoryId.nonContacts.rawValue:
|
||||||
default:
|
return .nonContacts
|
||||||
return nil
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.additionalPeerList.categories = Set(mappedCategories)
|
||||||
|
|
||||||
|
self.additionalPeerList.peers.removeAll()
|
||||||
|
for id in peerIds {
|
||||||
|
guard let maybePeer = peerMap[id], let peer = maybePeer else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
self.additionalPeerList.peers.append(AdditionalPeerList.Peer(
|
||||||
|
peer: peer,
|
||||||
|
isContact: isContactMap[id] ?? false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
self.additionalPeerList.peers.sort(by: { lhs, rhs in
|
||||||
|
return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle
|
||||||
|
})
|
||||||
|
|
||||||
|
let includedIds = self.additionalPeerList.peers.map(\.peer.id)
|
||||||
|
self.additionalPeerList.excludePeers.removeAll(where: { includedIds.contains($0.peer.id) })
|
||||||
|
} else {
|
||||||
|
self.additionalPeerList.excludePeers.removeAll()
|
||||||
|
for id in peerIds {
|
||||||
|
guard let maybePeer = peerMap[id], let peer = maybePeer else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
self.additionalPeerList.excludePeers.append(AdditionalPeerList.Peer(
|
||||||
|
peer: peer,
|
||||||
|
isContact: isContactMap[id] ?? false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
self.additionalPeerList.excludePeers.sort(by: { lhs, rhs in
|
||||||
|
return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle
|
||||||
|
})
|
||||||
|
|
||||||
|
let excludedIds = self.additionalPeerList.excludePeers.map(\.peer.id)
|
||||||
|
self.additionalPeerList.peers.removeAll(where: { excludedIds.contains($0.peer.id) })
|
||||||
}
|
}
|
||||||
|
|
||||||
self.additionalPeerList.categories = Set(mappedCategories)
|
|
||||||
|
|
||||||
self.additionalPeerList.peers.removeAll()
|
|
||||||
for id in peerIds {
|
|
||||||
guard let maybePeer = peerMap[id], let peer = maybePeer else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
self.additionalPeerList.peers.append(AdditionalPeerList.Peer(
|
|
||||||
peer: peer,
|
|
||||||
isContact: isContactMap[id] ?? false
|
|
||||||
))
|
|
||||||
}
|
|
||||||
self.additionalPeerList.peers.sort(by: { lhs, rhs in
|
|
||||||
return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle
|
|
||||||
})
|
|
||||||
self.state?.updated(transition: .immediate)
|
self.state?.updated(transition: .immediate)
|
||||||
|
|
||||||
controller?.dismiss()
|
controller?.dismiss()
|
||||||
@ -494,7 +525,7 @@ final class ChatbotSetupScreenComponent: Component {
|
|||||||
let navigationTitleSize = self.navigationTitle.update(
|
let navigationTitleSize = self.navigationTitle.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(MultilineTextComponent(
|
component: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(string: environment.strings.ChatbotSetup_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
|
text: .plain(NSAttributedString(string: environment.strings.ChatbotSetup_TitleItem, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||||
horizontalAlignment: .center
|
horizontalAlignment: .center
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -795,7 +826,7 @@ final class ChatbotSetupScreenComponent: Component {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.openAdditionalPeerListSetup()
|
self.openAdditionalPeerListSetup(isExclude: false)
|
||||||
}
|
}
|
||||||
))))
|
))))
|
||||||
for category in self.additionalPeerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) {
|
for category in self.additionalPeerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) {
|
||||||
@ -865,7 +896,7 @@ final class ChatbotSetupScreenComponent: Component {
|
|||||||
sideInset: 0.0,
|
sideInset: 0.0,
|
||||||
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
peer: peer.peer,
|
peer: peer.peer,
|
||||||
subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContact,
|
subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser,
|
||||||
subtitleAccessory: .none,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
@ -930,6 +961,103 @@ final class ChatbotSetupScreenComponent: Component {
|
|||||||
contentHeight += excludedSectionSize.height
|
contentHeight += excludedSectionSize.height
|
||||||
contentHeight += sectionSpacing
|
contentHeight += sectionSpacing
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
var excludedUsersContentHeight: CGFloat = 0.0
|
||||||
|
var excludedUsersSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||||
|
excludedUsersSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||||
|
theme: environment.theme,
|
||||||
|
title: AnyComponent(VStack([
|
||||||
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(
|
||||||
|
string: environment.strings.BusinessMessageSetup_Recipients_AddExclude,
|
||||||
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||||
|
textColor: environment.theme.list.itemAccentColor
|
||||||
|
)),
|
||||||
|
maximumNumberOfLines: 1
|
||||||
|
))),
|
||||||
|
], alignment: .left, spacing: 2.0)),
|
||||||
|
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||||
|
name: "Chat List/AddIcon",
|
||||||
|
tintColor: environment.theme.list.itemAccentColor
|
||||||
|
))),
|
||||||
|
accessory: nil,
|
||||||
|
action: { [weak self] _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.openAdditionalPeerListSetup(isExclude: true)
|
||||||
|
}
|
||||||
|
))))
|
||||||
|
for peer in self.additionalPeerList.excludePeers {
|
||||||
|
excludedUsersSectionItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(PeerListItemComponent(
|
||||||
|
context: component.context,
|
||||||
|
theme: environment.theme,
|
||||||
|
strings: environment.strings,
|
||||||
|
style: .generic,
|
||||||
|
sideInset: 0.0,
|
||||||
|
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||||
|
peer: peer.peer,
|
||||||
|
subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser,
|
||||||
|
subtitleAccessory: .none,
|
||||||
|
presence: nil,
|
||||||
|
selectionState: .none,
|
||||||
|
hasNext: false,
|
||||||
|
action: { peer, _, _ in
|
||||||
|
},
|
||||||
|
inlineActions: PeerListItemComponent.InlineActionsState(
|
||||||
|
actions: [PeerListItemComponent.InlineAction(
|
||||||
|
id: AnyHashable(0),
|
||||||
|
title: environment.strings.Common_Delete,
|
||||||
|
color: .destructive,
|
||||||
|
action: { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.additionalPeerList.excludePeers.removeAll(where: { $0.peer.id == peer.peer.id })
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
let excludedUsersSectionSize = self.excludedUsersSection.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(ListSectionComponent(
|
||||||
|
theme: environment.theme,
|
||||||
|
header: nil,
|
||||||
|
footer: AnyComponent(MultilineTextComponent(
|
||||||
|
text: .markdown(
|
||||||
|
text: environment.strings.ChatbotSetup_Recipients_ExcludedSectionFooter,
|
||||||
|
attributes: MarkdownAttributes(
|
||||||
|
body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
|
||||||
|
bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor),
|
||||||
|
link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor),
|
||||||
|
linkAttribute: { _ in
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
maximumNumberOfLines: 0
|
||||||
|
)),
|
||||||
|
items: excludedUsersSectionItems
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||||
|
)
|
||||||
|
let excludedUsersSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + excludedUsersContentHeight), size: excludedSectionSize)
|
||||||
|
if let excludedUsersSectionView = self.excludedUsersSection.view {
|
||||||
|
if excludedUsersSectionView.superview == nil {
|
||||||
|
self.scrollView.addSubview(excludedUsersSectionView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: excludedUsersSectionView, frame: excludedUsersSectionFrame)
|
||||||
|
transition.setAlpha(view: excludedUsersSectionView, alpha: !self.hasAccessToAllChatsByDefault ? 1.0 : 0.0)
|
||||||
|
}
|
||||||
|
excludedUsersContentHeight += excludedUsersSectionSize.height
|
||||||
|
excludedUsersContentHeight += sectionSpacing
|
||||||
|
if !self.hasAccessToAllChatsByDefault {
|
||||||
|
contentHeight += excludedUsersContentHeight
|
||||||
|
}
|
||||||
|
|
||||||
let permissionsSectionSize = self.permissionsSection.update(
|
let permissionsSectionSize = self.permissionsSection.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(ListSectionComponent(
|
component: AnyComponent(ListSectionComponent(
|
||||||
@ -1107,6 +1235,7 @@ public final class ChatbotSetupScreen: ViewControllerComponentContainer {
|
|||||||
|
|
||||||
var additionalPeerIds = Set<EnginePeer.Id>()
|
var additionalPeerIds = Set<EnginePeer.Id>()
|
||||||
additionalPeerIds.formUnion(connectedBot.recipients.additionalPeers)
|
additionalPeerIds.formUnion(connectedBot.recipients.additionalPeers)
|
||||||
|
additionalPeerIds.formUnion(connectedBot.recipients.excludePeers)
|
||||||
|
|
||||||
return context.engine.data.get(
|
return context.engine.data.get(
|
||||||
TelegramEngine.EngineData.Item.Peer.Peer(id: connectedBot.id),
|
TelegramEngine.EngineData.Item.Peer.Peer(id: connectedBot.id),
|
||||||
|
@ -170,10 +170,16 @@ private final class ChatManagingBotTitlePanelComponent: Component {
|
|||||||
containerSize: CGSize(width: maxTextWidth, height: 100.0)
|
containerSize: CGSize(width: maxTextWidth, height: 100.0)
|
||||||
)
|
)
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
|
let textValue: String
|
||||||
|
if component.isPaused {
|
||||||
|
textValue = "bot paused"
|
||||||
|
} else {
|
||||||
|
textValue = component.managesChat ? "bot manages this chat" : "bot has access to this chat"
|
||||||
|
}
|
||||||
let textSize = self.text.update(
|
let textSize = self.text.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(MultilineTextComponent(
|
component: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(string: component.managesChat ? "bot manages this chat" : "bot has access to this chat", font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor))
|
text: .plain(NSAttributedString(string: textValue, font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor))
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: maxTextWidth, height: 100.0)
|
containerSize: CGSize(width: maxTextWidth, height: 100.0)
|
||||||
@ -400,7 +406,7 @@ final class ChatManagingBotTitlePanelNode: ChatTitleAccessoryPanelNode {
|
|||||||
strings: interfaceState.strings,
|
strings: interfaceState.strings,
|
||||||
insets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: 0.0, right: rightInset),
|
insets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: 0.0, right: rightInset),
|
||||||
peer: managingBot.bot,
|
peer: managingBot.bot,
|
||||||
managesChat: managingBot.canReply,
|
managesChat: managingBot.canReply || managingBot.isPaused,
|
||||||
isPaused: managingBot.isPaused,
|
isPaused: managingBot.isPaused,
|
||||||
toggleIsPaused: { [weak self] in
|
toggleIsPaused: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user